环境:Java8, Idea 社区版,ubuntu 18.04 LTS
背景:基本没有 Java 实战经验,有 Python 和 Golang 的经验
前篇
Basic-Data-Type 常见类型有
byte (8)
short (16)
int (32)
long (64)
float (32)
double (64)
boolean
char (16)
需要注意的是,局部变量声明后需要进行初始化,否则将报错。但作为类的成员则有默认值,如 int 不初始化则默认为 0。
Encoding 我在本机环境下进行测试时,JVM 默认的编码为 UTF-8。如果对编码比较敏感,可以通过设置环境变量修改编码,这里有个有趣的例子
1 2 3 4 5 6 7 8 9 10 11 12 public static void main (String[] args) throws IOException { SecureRandom random = new SecureRandom (); byte [] src = new byte [16 ]; random.nextBytes(src); System.out.println(src.length); String dst = new String (src); System.out.println(dst.length()); System.out.println(dst.getBytes().length); System.out.println(dst.getBytes(StandardCharsets.UTF_16).length); }
上述的结果除了第一个是明确的 16 以外其它都是不明确的,对于一个 byte,UTF-8 可能会以 2 个 byte 编码,所以上述代码的 dst 则是不确定长度的。但一般来说这种场景比较少见,在实际写代码的过程中,我们使用的是编码过后的文件或者输入,而不是直接处理 unicode。
Array Array 为定长的固定类型的数组。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int [] array; array = new int [1 ]; array[0 ] = 1 ; System.out.println(array);int [] list = {1 , 2 , 3 , 4 }; System.out.println(list); String[][] names = { {"Mr. " , "Mrs. " , "Ms. " }, {"Smith" , "Jones" } };
在 java.util.Arrays
中包含众多 array 的有用的方法
1 2 3 4 5 6 7 8 9 int [] src = {1 , 2 , 3 , 4 , 5 , 6 , 7 };int [] dst = Arrays.copyOfRange(src, 0 , 5 ); Arrays.sort(dst); for (int item: dst) { System.out.println(item); } System.out.println(Arrays.binarySearch(src, 8 ));
Nested-Classes 嵌套类分两种,static 的和 non-static 的。前者为静态嵌套类,后者为内部类。
Static-Nested-Classes 静态嵌套类(下称嵌套类)为包含其的类(这里称为外部类)的一个成员。
嵌套类能访问外部类的 static 属性(包括 private)和方法,即与该类关联,而不是与该类的实例管理。常用来实现外部类相关的一些 helper 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class OuterClass { private static int objCnt = 0 ; public static class StaticNestedClass { public int getObjCnt () { return objCnt; } } public OuterClass () { objCnt +=1 ; } }
Inner-Classes 内部类的实例与外部类的实例绑定,与嵌套类不同的是,内部类不允许有 static 的成员。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class OuterClass { public int v = 0 ; public int x = 0 ; public class InnerClass { public int v = 1 ; public void testPrint () { System.out.println("this.v = " + v); System.out.println("this.v = " + this .v); System.out.println("OuterClass.this.x = " + x); System.out.println("OuterClass.this.v = " + OuterClass.this .v); } } }
上述 this 的用法与之前说过的类似,用于指明访问的是哪一个变量。如果内部类定义了与外部类同名的成员,则直接使用会屏蔽外部类对应的成员。
Local-Classes 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class Main { public static void main (String[] args) { String word = "word" ; class Test { public void print () { System.out.println(word); } } Test t = new Test (); t.print(); } }
本地类用于用完即弃的场景。
需要注意的是,在本地类中可以访问外部的局部变量,前提是这些变量是 final 的或者是 effectively final。effectively final 为 Java8 新增的特性,即变量初始化时编译器都当作 final,但在更新时变成了 non-final。
同样的,如果本地类中定义了同名的成员,则会屏蔽外部的变量。
Anonymous-Classes 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Main { interface Greeter { public void sayHi () ; } public static void main (String[] args) { Greeter helloWorld = new Greeter () { public void sayHi () { System.out.println("hello-world" ); } }; helloWorld.sayHi(); } }
同内部类和本地类,匿名类都不能有 static 属性 / 方法,但是可以有 final static
的属性。匿名类的常见例子如实现自定义排序的规则。
1 2 3 4 5 6 7 Integer[] ls = {1 , 4 , 2 , 3 , 0 }; Arrays.sort(ls, new Comparator <Integer>() { @Override public int compare (Integer x, Integer y) { return x - y; } });
Lambda 上述的排序可以以 Lambda 表达式改写
1 Arrays.sort(ls, (x, y) -> x - y);
Lambda 表达式基本语法如下
1 2 3 4 5 (arg1, arg2...) -> { body }(type1 arg1, type2 arg2...) -> { body }() -> { body }
这里还有另一个常见的例子,用于过滤数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import java.util.Arrays;import java.util.List;interface Predicate <T> { boolean test (T t) ; }public class Main { public static void main (String[] args) { List<Integer> list = Arrays.asList(1 , 2 , 3 , 4 , 5 , 6 , 7 ); evaluate(list, x -> x > 1 ); evaluate(list, x -> true ); list.forEach(System.out::println); } public static void evaluate (List<Integer> list, Predicate<Integer> predicate) { for (Integer n : list) { if (predicate.test(n)) { System.out.print(n + " " ); } } System.out.println(); } }
基本的规则如下
可带参数,也可不带参数
可带类型,也可不带类型,根据上下文来推导类型
Lambda 更多与 Interface 细节相关,这里先只介绍相关语法。
Annotation 注解,对被注解代码的逻辑没有直接的影响,而是在逻辑以外提供额外的数据信息。
可提供信息给编译器使用
IDE 可根据注解生成对应的配置文件
其信息可被编译进 class 文件中,或者说保留在 Java 虚拟机中,供运行时判断
对于 Java 代码从编写到运行有三个时期
代码编辑
编译成 .class 文件
读取到 JVM 运行
针对这三个时期有三种 Annotation 对应
RetentionPolicy.SOURCE // 只在代码编辑期生效
RetentionPolicy.CLASS // 在编译期生效,默认值
RetentionPolicy.RUNTIME // 在代码运行时生效
除此之外,Java 提供了 @Target 这个 元注解 来指定某个 Annotation 修饰的目标对象,如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) @Retention(RetentionPolicy.SOURCE) public @interface SuppressWarnings { String[] value(); }
SuppressWarnings
这个注解作用于类型函数等等,并且在代码编辑期间生效。代码编辑期间的注解更多是用于代码检查,如更常见的 Override
。
Runtime-Annotation runtime annotation 是基于 Java 本身的反射机制来实现,反射指的是在运行期间动态操作类 / 对象,这里先不展开来讲。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @interface JsonKeyField { String value () default "" ; }public class Person { @JsonKeyField("first_name") public String firstName; @JsonKeyField("last_name") public String lastName; @JsonKeyField() public int age; public Person (String firstName, String lastName, int age) { this .firstName = firstName; this .lastName = lastName; this .age = age; } public String toJsonStr () throws JsonProcessingException, IllegalAccessException { Map<String, Object> map = new HashMap <String, Object>(); for (Field field : this .getClass().getDeclaredFields()) { if (field.isAnnotationPresent(JsonKeyField.class)) { JsonKeyField jkf = field.getAnnotation(JsonKeyField.class); String key = jkf.value().isEmpty()? field.getName(): jkf.value(); map.put(key, field.get(this )); } } return new ObjectMapper ().writeValueAsString(map); } }
上述代码使用注解 JsonKeyField
来规范导出成 JSON 时候的字段,如果没有指定,则沿用原有的成员名字。这里的写法有点类似 Golang 中的 struct tag 的功能,如果有多种格式(如 Msgpack)的需求可以写多个注解。
其实可以看到,注解本身其实只起到一个标记或者说文档的作用,而真正的用处是供外部调用在运行时提供信息给外部进行判断来执行相应的操作。
需要注意的是,注解中只有一个属性,使用 value()
来定义,则使用注解时可以不用指定参数名。常见的用于代码检查的注解和用于运行时检查的注解,还有编译时使用的注解,后者这边就不进行讨论了。
Generics 泛型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class GenericBox <T> { private T item; public T getItem () { return item; } public void setItem (T item) { this .item = item; } }
这里需要注意的是,泛型中的类型不能是基础类型(如 int,char),你可以是任何的类,接口。
这里有个约定的类型命名
E - Element (used extensively by the Java Collections Framework)
K - Key
N - Number
T - Type
V - Value
S,U,V etc. - 2nd, 3rd, 4th types
Generic-Method 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 interface Pair <K, V> { public K getKey () ; public V getValue () ; }class OrderedPair <K, V> implements Pair <K, V> { private K key; private V value; public OrderedPair (K key, V value) { this .key = key; this .value = value; } public K getKey () { return key; } public V getValue () { return value; } public static <K, V> boolean equals (Pair<K, V> p1, Pair<K, V> p2) { return true ; } }
与普通的函数不同的是,上述的泛型方法声明时前面需要添加 <T...>
Bounded-Type-Parameter 当使用泛型时,如尝试在函数内进行比较操作,如果一些自定义类没有实现 compare 相关的方法,则会在运行时报错。这里可以引入泛型边界的用法。
1 2 3 4 5 6 7 8 9 10 11 12 13 class Box <T extends Number > { private T item; public T getItem () { return item; } public void setItem (T item) { this .item = item; } }
如果使用类不是 Number 或者不是继承了 Number 的类则会在编译时报错。此外也可以添加多个约束,语法为 <T extends B1 & B2 & B3>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Data implements Comparable <Data> { int data; public int compareTo (Data d) { return data - d.data; } public int function (Box<Double> bi) { return 1 ; } }class Box <T extends Comparable <T>> { private T item; public T getItem () { return item; } public void setItem (T item) { this .item = item; } public static void f (List<Number> l) { ; } }
也可以使用接口去限制,这里则是要求必须实现 Comparable
接口。这里需要注意的是,像上述代码 Box
中像方法 f
如果试图传入 List<Integer>
则会报错。
Integer
的确是 Number
子类,但 Box<Integer>
不是 Box<Number>
的子类,准确来说两者没有任何关系,他们的父类均是 Object
。另一方面 ArrayList<String>
的父类是 List<String>
并以此类推。
Type-Inference 类型推断
1 2 Map<String, List<String>> myMap = new HashMap <String, List<String>>(); Map<String, List<String>> myMap1 = new HashMap <>();
上述两种调用都是合法的,需要注意的是 HashMap
后面的 <>
不能忽略。这里涉及更多是编译器的优化,如
1 2 3 4 5 6 void processStringList (List<String> stringList) { }
Java10 更是引进了 var 的类型判断机制,在我看来,写代码的时候借助 IDE 工具一般就能解决这些问题。
Wildcard 通配符,这里有三种通配符
Upper Bounded Wildcard
Unbounded Wildcard
Lower Bounded Wildcard
其对应的语法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public static double sumOfList (List<? extends Number> list) { double s = 0.0 ; for (Number n : list) s += n.doubleValue(); return s; }public static void printList (List<?> list) { for (Object elem: list) System.out.print(elem + " " ); System.out.println(); }public static void addNumbers (List<? super Integer> list) { for (int i = 1 ; i <= 10 ; i++) { list.add(i); } }
通配符的使用主要是为了解决诸如 Box<Integer>
不是 Box<Number>
的子类的问题。
对于选择 Upper 还是 Lower,可以以严于律己宽于待人来设计,即
对于外部输入,用户调用的参数,选择 Upper Bounded Wildcard,它最终也可以是 null 或者 Object 的子类
对于函数的输出,则应该使用 Lower Bounded Wildcard
Type-Erasure 类型擦除是 Java 泛型的实现方法。编译器在编译的时候去掉了泛型的信息(在 这里 可以看到编译和反编译代码的区别)。
和 C++ 不同,C++ 会为每一种类型生成对应的函数,而 Java 则只生成一份代码(以 Object 替代自定义类型 T,以及在某些地方添加类型转换)。
References
Difference Between Final And Effectively Final
深入浅出 Java 8 Lambda
Java 8 Lambda 表达式详解
Annotation 详解
Java 泛型和类型擦除
Java 类型擦除
Java Encoding 解析