List 是最基础的一种集合:它是一种有序列表。 List 的行为和数组几乎完全相同:List 内部按照放入元素的先后顺序存放,每个元素都可以通过索引确定自己的位置,List 的索引和数组一样,从 0 开始。 数组和 List 类似,也是有序结构,如果我们使用数组,在添加和删除元素的时候,会非常不方便。
publicclassMain { publicstaticvoidmain(String[] args) { List<String> list = List.of("apple", "pear", "banana"); for (Iterator<String> it = list.iterator(); it.hasNext(); ) { Strings= it.next(); System.out.println(s); } } }
有童鞋可能觉得使用 Iterator 访问 List 的代码比使用索引更复杂。但是,要记住,通过 Iterator 遍历 List 永远是最高效的方式。并且,由于 Iterator 遍历是如此常用,所以,Java 的 for each 循环本身就可以帮我们使用 Iterator 遍历。把上面的代码再改写如下:
1 2 3 4 5 6 7 8
publicclassMain { publicstaticvoidmain(String[] args) { List<String> list = List.of("apple", "pear", "banana"); for (String s : list) { System.out.println(s); } } }
上述代码就是我们编写遍历 List 的常见代码。
实际上,只要实现了 Iterable 接口的集合类都可以直接用 for each 循环来遍历,Java 编译器本身并不知道如何遍历集合对象,但它会自动把 for each 循环变成 Iterator 的调用,原因就在于 Iterable 接口定义了一个 Iterator<E> iterator() 方法,强迫集合类必须返回一个 Iterator 实例。
publicclassMain { publicstaticvoidmain(String[] args) { List<Integer> list = List.of(12, 34, 56); Integer[] array = list.toArray(newInteger[3]); for (Integer n : array) { System.out.println(n); } } }
注意到这个 toArray(T[]) 方法的泛型参数 <T> 并不是 List 接口定义的泛型参数 <E>,所以,我们实际上可以传入其他类型的数组,例如我们传入 Number 类型的数组,返回的仍然是 Number 类型: 注意到这个 toArray(T[]) 方法的泛型参数 <T> 并不是 List 接口定义的泛型参数 <E>,所以,我们实际上可以传入其他类型的数组,例如我们传入 Number 类型的数组,返回的仍然是 Number 类型:
1 2 3 4 5 6 7 8 9
publicclassMain { publicstaticvoidmain(String[] args) { List<Integer> list = List.of(12, 34, 56); Number[] array = list.toArray(newNumber[3]); for (Number n : array) { System.out.println(n); } } }
但是,如果我们传入类型不匹配的数组,例如,String[]类型的数组,由于 List 的元素是 Integer,所以无法放入 String 数组,这个方法会抛出 ArrayStoreException。 如果我们传入的数组大小和 List 实际的元素个数不一致怎么办?根据 List 接口的文档,我们可以知道: 如果传入的数组不够大,那么 List 内部会创建一个新的刚好够大的数组,填充后返回;如果传入的数组比 List 元素还要多,那么填充完元素后,剩下的数组元素一律填充 null。 实际上,最常用的是传入一个 “恰好” 大小的数组:
重复放入key-value并不会有任何问题,但是一个key只能关联一个value。在上面的代码中,一开始我们把key对象”apple”映射到Integer对象123,然后再次调用put()方法把”apple”映射到789,这时,原来关联的value对象123就被“冲掉”了。实际上,put()方法的签名是V put(K key, V value),如果放入的key已经存在,put()方法会返回被删除的旧的value,否则,返回null。
publicclassPair<T> { private T first; private T last; publicPair(T first, T last) { this.first = first; this.last = last; } public T getFirst() { return first; } public T getLast() { return last; } }
熟练后即可直接从T开始编写。
静态方法
编写泛型类时,要特别注意,泛型类型 <T> 不能用于静态方法。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
publicclassPair<T> { private T first; private T last; publicPair(T first, T last) { this.first = first; this.last = last; } public T getFirst() { ... } public T getLast() { ... }
publicclassPair<T> { private T first; private T last; publicPair(T first, T last) { this.first = first; this.last = last; } public T getFirst() { ... } public T getLast() { ... }
publicclassPair<T> { private T first; private T last; publicPair(T first, T last) { this.first = first; this.last = last; } public T getFirst() { ... } public T getLast() { ... }
publicclassPair<T, K> { private T first; private K last; publicPair(T first, K last) { this.first = first; this.last = last; } public T getFirst() { ... } public K getLast() { ... } }
publicclassPair<T> { private T first; private T last; publicPair(T first, T last) { this.first = first; this.last = last; } public T getFirst() { return first; } public T getLast() { return last; } }
classPair<T> { private T first; private T last; publicPair(T first, T last) { this.first = first; this.last = last; } public T getFirst() { return first; } public T getLast() { return last; } }
因为 T 是 Object,我们对 Pair<String> 和 Pair<Integer> 类型获取 Class 时,获取到的是同一个 Class,也就是 Pair 类的 Class。 换句话说,所有泛型实例,无论 T 的类型是什么,getClass() 返回同一个 Class 实例,因为编译后它们全部都是 Pair<Object>。
局限三:无法判断带泛型的类型:
1 2 3 4
Pair<Integer> p = newPair<>(123, 456); // Compile error: if (p instanceof Pair<String>) { }
原因和前面一样,并不存在Pair<String>.class,而是只有唯一的Pair.class。
局限四:不能实例化T类型:
1 2 3 4 5 6 7 8 9
publicclassPair<T> { private T first; private T last; publicPair() { // Compile error: first = newT(); last = newT(); } }
上述代码无法通过编译,因为构造方法的两行语句:
1 2
first = newT(); last = newT();
擦拭后实际上变成了:
1 2
first = newObject(); last = newObject();
这样一来,创建 new Pair<String>() 和创建 new Pair<Integer>() 就全部成了 Object,显然编译器要阻止这种类型不对的代码。 要实例化 T 类型,我们必须借助额外的 Class<T> 参数:
1 2 3 4 5 6 7 8
publicclassPair<T> { private T first; private T last; publicPair(Class<T> clazz) { first = clazz.newInstance(); last = clazz.newInstance(); } }
classPair<T> { private T first; private T last; publicPair(T first, T last) { this.first = first; this.last = last; } public T getFirst() { return first; } public T getLast() { return last; } }
classPair<T> { private T first; private T last; publicPair(T first, T last) { this.first = first; this.last = last; } public T getFirst() { return first; } public T getLast() { return last; } }
直接运行,会得到一个编译错误:
1
incompatible types: Pair<Integer> cannot be converted to Pair<Number>
classPair<T> { private T first; private T last; publicPair(T first, T last) { this.first = first; this.last = last; } public T getFirst() { return first; } public T getLast() { return last; } }
这样一来,给方法传入 Pair<Integer> 类型时,它符合参数 Pair<? extends Number> 类型。这种使用 <? extends Number > 的泛型定义称之为上界通配符(Upper Bounds Wildcards),即把泛型类型 T 的上界限定在 Number 了。 除了可以传入 Pair<Integer> 类型,我们还可以传入 Pair<Double> 类型,Pair<BigDecimal> 类型等等,因为 Double 和 BigDecimal 都是 Number 的子类。 如果我们考察对 Pair<? extends Number> 类型调用 getFirst() 方法,实际的方法签名变成了:
它的作用是把一个List的每个元素依次添加到另一个List中。它的第一个参数是List<? super T>,表示目标List,第二个参数List<? extends T>,表示要复制的List。我们可以简单地用for循环实现复制。在for循环中,我们可以看到,对于类型<? extends T>的变量src,我们可以安全地获取类型T的引用,而对于类型<? super T>的变量dest,我们可以安全地传入T的引用。 这个copy()方法的定义就完美地展示了extends和super的意图:
如果我们不捕获 UnsupportedEncodingException,会出现编译失败的问题 编译器会报错,错误信息类似:unreported exception UnsupportedEncodingException; must be caught or declared to be thrown,并且准确地指出需要捕获的语句是 return s.getBytes(“GBK”);。意思是说,像 UnsupportedEncodingException 这样的 Checked Exception,必须被捕获。 这是因为 String.getBytes(String) 方法定义是:
java.lang.NumberFormatException: null at java.lang.Integer.parseInt(Integer.java:542) at java.lang.Integer.parseInt(Integer.java:615) at class3.module3.Test1.process2(Test1.java:23) at class3.module3.Test1.process1(Test1.java:19) at class3.module3.Test1.main(Test1.java:12)
java.lang.IllegalArgumentException: java.lang.NullPointerException at Main.process1(Main.java:15) at Main.main(Main.java:5) Caused by: java.lang.NullPointerException at Main.process2(Main.java:20) at Main.process1(Main.java:13)
catched finally Exception in thread "main" java.lang.RuntimeException: java.lang.NumberFormatException: For input string: "abc" at Main.main(Main.java:8) Caused by: java.lang.NumberFormatException: For input string: "abc" at ...
Exception in thread "main" java.lang.IllegalArgumentException at Main.main(Main.java:11) Suppressed: java.lang.NumberFormatException: For input string: "abc" at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) at java.base/java.lang.Integer.parseInt(Integer.java:652) at java.base/java.lang.Integer.parseInt(Integer.java:770) at Main.main(Main.java:6)
语句 assert x >= 0; 即为断言,断言条件 x >= 0 预期为 true。如果计算结果为 false,则断言失败,抛出 AssertionError。 使用 assert 语句时,还可以添加一个可选的断言消息:
1
assert x >= 0 : "x must >= 0";
这样,断言失败的时候,AssertionError 会带上消息 x must >= 0,更加便于调试。 Java 断言的特点是:断言失败时会抛出 AssertionError,导致程序结束退出。因此,断言不能用于可恢复的程序错误,只应该用于开发和测试阶段。 对于可恢复的程序错误,不应该使用断言。例如:
1 2 3
voidsort(int[] arr) { assert arr != null; }
应该抛出异常并在上层捕获:
1 2 3 4 5
voidsort(int[] arr) { if (x == null) { thrownewIllegalArgumentException("array cannot be null"); } }
publicclassHello { publicstaticvoidmain(String[] args) { Loggerlogger= Logger.getGlobal(); logger.info("start process..."); logger.warning("memory is running out..."); logger.fine("ignored."); logger.severe("process will be terminated..."); } }
输出
1 2 3 4 5 6
Jan 15, 2021 5:57:27 AM Hello main INFO: start process... Jan 15, 2021 5:57:27 AM Hello main WARNING: memory is running out... Jan 15, 2021 5:57:27 AM Hello main SEVERE: process will be terminated...
运行上述代码,肯定会得到编译错误,类似error: package org.apache.commons.logging does not exist(找不到org.apache.commons.logging这个包)。因为Commons Logging是一个第三方提供的库,所以,必须先把它下载下来。下载后,解压,找到commons-logging-1.2.jar这个文件,再把Java源码Main.java放到一个目录下,例如work目录:
最后一个问题是,Maven 如何知道从何处下载所需的依赖?也就是相关的 jar 包?答案是 Maven 维护了一个中央仓库(repo1.maven.org),所有第三方库将自身的 jar 以及相关信息上传至中央仓库,Maven 就可以从中央仓库把所需依赖下载到本地。 Maven并不会每次都从中央仓库下载jar包。一个jar包一旦被下载过,就会被Maven自动缓存在本地目录(用户主目录的.m2目录),所以,除了第一次编译时因为下载需要时间会比较慢,后续过程因为有本地缓存,并不会重复下载相同的jar包。
publicclassMain { publicstaticvoidmain(String[] args) { System.out.println("main start..."); // main Threadt=newThread() { // main publicvoidrun() { System.out.println("thread run..."); System.out.println("thread end."); } }; t.start(); // main System.out.println("main end..."); // main } }
我们用 // main 表示主线程,也就是 main线程 ,main线程 执行的代码有 4 行,首先打印 main start,然后创建 Thread 对象,紧接着调用 start() 启动新线程。当 start() 方法被调用时,JVM 就创建了一个新线程,我们通过实例变量 t 来表示这个新线程对象,并开始执行。 接着,main 线程继续执行打印 main end 语句,而 t 线程在 main 线程执行的同时会并发执行,打印 thread run 和 thread end 语句。
当 main 线程对线程对象 t 调用 join() 方法时,主线程将等待变量 t 表示的线程运行结束,即 join 就是指等待该线程结束,然后才继续往下执行自身线程。所以,上述代码打印顺序可以肯定是 main 线程先打印 start,t 线程再打印 hello,main 线程最后再打印 end。 如果 t 线程已经结束,对实例 t 调用 join() 会立刻返回。此外,join(long) 的重载方法也可以指定一个等待时间,超过等待时间后就不再继续等待。
classMyThreadextendsThread { publicvoidrun() { intn=0; while (! isInterrupted()) { n ++; System.out.println(n + " hello!"); } } }
仔细看上述代码,main 线程通过调用 t.interrupt()方法中断 t 线程,但是要注意,interrupt()方法仅仅向 t 线程发出了 “中断请求”,至于 t 线程是否能立刻响应,要看具体代码。而 t 线程的 while 循环会检测 isInterrupted(),所以上述代码能正确响应 interrupt() 请求,使得自身立刻结束运行 run()方法。
如果线程处于等待状态,例如,t.join()会让 main 线程进入等待状态,此时,如果对 main 线程调用 interrupt(),join()方法会立刻抛出 InterruptedException,因此,目标线程只要捕获到 join()方法抛出的 InterruptedException,就说明有其他线程对其调用了 interrupt()方法,通常情况下该线程应该立刻结束运行。
main 线程通过调用 t.interrupt() 从而通知 t 线程中断,而此时 t 线程正位于 hello.join() 的等待中,此方法会立刻结束等待并抛出 InterruptedException。由于我们在 t 线程中捕获了 InterruptedException,因此,就可以准备结束该线程。在 t 线程结束前,对 hello 线程也进行了 interrupt() 调用通知其中断。如果去掉这一行代码,可以发现 hello 线程仍然会继续运行,且 JVM 不会退出。
这会导致如果一个线程更新了某个变量,另一个线程读取的值可能还是更新前的。例如,主内存的变量 a = true,线程 1 执行 a = false 时,它在此刻仅仅是把变量 a 的副本变成了 false,主内存的变量 a 还是 true,在 JVM 把修改后的 a 回写到主内存之前,其他线程读取到的 a 的值仍然是 true,这就造成了多线程之间共享的变量不一致。
age public int Person.getAge() public void Person.setAge(int) child public boolean Person.isChild() null class public final native java.lang.Class java.lang.Object.getClass() null name public java.lang.String Person.getName() public void Person.setName(java.lang.String)
publicclassMain { publicstaticvoidmain(String[] args) { Weekdayday= Weekday.SUN; if (day.dayValue == 6 || day.dayValue == 0) { System.out.println("Today is " + day + ". Work at home!"); } else { System.out.println("Today is " + day + ". Work at office!"); } } }
publicclassMain { publicstaticvoidmain(String[] args) { Weekdayday= Weekday.SUN; switch(day) { case MON: case TUE: case WED: case THU: case FRI: System.out.println("Today is " + day + ". Work at office!"); break; case SAT: case SUN: System.out.println("Today is " + day + ". Work at home!"); break; default: thrownewRuntimeException("cannot process " + day); } } }
Java 使用 enum 定义枚举类型,它被编译器编译为 final class Xxx extends Enum {…};
通过 name() 获取常量定义的字符串,注意不要使用 toString();
通过 ordinal() 返回常量定义的顺序(无实质意义);
可以为 enum 编写构造方法、字段和方法
enum 的构造方法要声明为 private,字段强烈建议声明为 final;
enum 适合用在 switch 语句中。
BigInteger
在 Java 中,由 CPU 原生提供的整型最大范围是 64 位 long 型整数。使用 long 型整数可以直接通过 CPU 指令进行计算,速度非常快。 如果我们使用的整数范围超过了 long 型怎么办?这个时候,就只能用软件来模拟一个大整数。java.math.BigInteger 就是用来表示任意大小的整数。BigInteger 内部用一个 int[] 数组来模拟一个非常大的整数:
BigIntegeri=newBigInteger("123456789000"); System.out.println(i.longValue()); // 123456789000 System.out.println(i.multiply(i).longValueExact()); // java.lang.ArithmeticException: BigInteger out of long range
使用 longValueExact() 方法时,如果超出了 long 型的范围,会抛出 ArithmeticException。 BigInteger 和 Integer、Long 一样,也是不可变类,并且也继承自 Number 类。因为 Number 定义了转换为基本类型的几个方法:
Java 标准库还提供了一个 StrictMath,它提供了和 Math 几乎一模一样的方法。这两个类的区别在于,由于浮点数计算存在误差,不同的平台(例如 x86 和 ARM)计算的结果可能不一致(指误差不同),因此,StrictMath 保证所有平台计算结果都是完全相同的,而 Math 会尽量针对平台优化计算速度,所以,绝大多数情况下,使用 Math 就足够了。
Random
Random 用来创建伪随机数。所谓伪随机数,是指只要给定一个初始的种子,产生的随机数序列是完全一样的。
这个 Book 类也有 name 字段,那么,我们能不能让 Student 继承自 Book 呢?不可以 从逻辑上讲,这是不合理的,Student 不应该从 Book 继承,而应该从 Person 继承。 究其原因,是因为 Student 是 Person 的一种,它们是 is 关系,而 Student 并不是 Book。实际上 Student 和 Book 的关系是 has 关系。 具有 has 关系不应该使用继承,而是使用组合,即 Student 可以持有一个 Book 实例:
1 2 3 4
classStudentextendsPerson { protected Book book; protectedint score; }
## 静态字段 > 在一个 class 中定义的字段,我们称之为实例字段。实例字段的特点是,每个实例都有独立的字段,各个实例的同名字段互不影响。
> 还有一种字段,是用 static 修饰的字段,称为静态字段:static field。 > 实例字段在每个实例中都有自己的一个独立 “空间”,但是静态字段只有一个共享 “空间”,所有实例都会共享该字段。举个例子: ```java class Person { public String name; public int age; // 定义静态字段 number: public static int number; }
## 接口的静态字段 > 因为 interface 是一个纯抽象类,所以它不能定义实例字段。但是,interface 是可以有静态字段的,并且静态字段必须为 final 类型: ```java public interface Person { public static final int MALE = 1; public static final int FEMALE = 2; }
实际上,因为 interface 的字段只能是 public static final 类型,所以我们可以把这些修饰符都去掉,上述代码可以简写为:
1 2 3 4 5
publicinterfacePerson { // 编译器会自动加上 public statc final: intMALE=1; intFEMALE=2; }
编译器会自动把该字段变为 public static final 类型。
小结
静态字段属于所有实例 “共享” 的字段,实际上是属于 class 的字段;
调用静态方法不需要实例,无法访问 this,但可以访问静态字段和其他静态方法;
静态方法常用于工具类和辅助方法。
包
定义
在前面的代码中,我们把类和接口命名为 Person、Student、Hello 等简单名字。
在现实中,如果小明写了一个 Person 类,小红也写了一个 Person 类,现在,小白既想用小明的 Person,也想用小红的 Person,怎么办?
public class Hello { void hi(String name) { // ① String s = name.toLowerCase(); // ② int len = s.length(); // ③ if (len < 10) { // ④ int p = 10 - len; // ⑤ for (int i=0; i<10; i++) { // ⑥ System.out.println(); // ⑦ } // ⑧ } // ⑨ } // ⑩ }
我们观察上面的 hi() 方法代码:
方法参数 name 是局部变量,它的作用域是整个方法,即 ①~⑩;
变量 s 的作用域是定义处到方法结束,即 ②~⑩;
变量 len 的作用域是定义处到方法结束,即 ③~⑩;
变量 p 的作用域是定义处到 if 块结束,即 ⑤~⑨;
变量 i 的作用域是 for 循环,即 ⑥~⑧。
final
final 与访问权限不冲突,它有很多作用。
用 final 修饰 class 可以阻止被继承:
1
package abc;
// 无法被继承: public final class Hello { private int n = 0; protected void hi(int t) { long i = t; } }
这是因为 Inner Class 除了有一个 this 指向它自己,还隐含地持有一个 Outer Class 实例,可以用 Outer.this 访问这个实例。所以,实例化一个 Inner Class 不能脱离 Outer 实例。 Inner Class 和普通 Class 相比,除了能引用 Outer 实例外,还有一个额外的 “特权”,就是可以修改 Outer Class 的 private 字段,因为 Inner Class 的作用域在 Outer Class 内部,所以能访问 Outer Class 的 private 字段和方法。
观察 Java 编译器编译后的. class 文件可以发现,Outer 类被编译为 Outer.class,而 Inner 类被编译为 Outer$Inner.class。
Anonymous Class (?)
还有一种定义 Inner Class 的方法,它不需要在 Outer Class 中明确地定义这个 Class,而是在方法内部,通过匿名类(Anonymous Class)来定义。示例代码如下:
## Static Nested Class > 最后一种内部类和 Inner Class 类似,但是使用 static 修饰,称为静态内部类(Static Nested Class): ```java public class Main { public static void main(String[] args) { Outer.StaticNested sn = new Outer.StaticNested(); sn.hello(); } }
class Outer { private static String NAME = "OUTER";
JVM 根据 classpath 设置的. 在当前目录下查找 com.example.Hello,即实际搜索文件必须位于 com/example/Hello.class。如果指定的. class 文件不存在,或者目录结构和包名对不上,均会报错。
jar 包
如果有很多 .class 文件,散落在各层目录中,肯定不便于管理。如果能把目录打一个包,变成一个文件,就方便多了。 jar 包就是用来干这个事的,它可以把 package 组织的目录层级,以及各个目录下的所有文件(包括. class 文件和其他文件)都打成一个 jar 文件,这样一来,无论是备份,还是发给客户,就简单多了。 jar 包实际上就是一个 zip 格式的压缩文件,而 jar 包相当于目录。如果我们要执行一个 jar 包的 class,就可以把 jar 包放到 classpath 中:
1
java -cp ./hello.jar abc.xyz.Hello
这样 JVM 会自动在 hello.jar 文件里去搜索某个类。
那么问题来了:如何创建 jar 包? 因为 jar 包就是 zip 包,所以,直接在资源管理器中,找到正确的目录,点击右键,在弹出的快捷菜单中选择 “发送到”,“压缩 (zipped) 文件夹”,就制作了一个 zip 文件。然后,把后缀从 .zip 改为 .jar,一个 jar 包就创建成功。
假设编译输出的目录结构是这样:
1 2 3 4 5 6 7 8 9
package_sample └─ bin ├─ hong │ └─ Person.class │ ming │ └─ Person.class └─ mr └─ jun └─ Arrays.class
这里需要特别注意的是,jar 包里的第一层目录,不能是 bin,而应该是 hong、ming、mr。如果在 Windows 的资源管理器中看,应该长这样:
1 2 3 4 5 6 7 8 9 10
package_sample └─ bin └─ hello.zip ├─ hong │ └─ Person.class │ ming │ └─ Person.class └─ mr └─ jun └─ Arrays.class
如果长这样:
1 2 3 4 5 6 7 8 9 10 11
package_sample └─ bin └─ hello.zip └─ bin ├─ hong │ └─ Person.class │ ming │ └─ Person.class └─ mr └─ jun └─ Arrays.class
说明打包打得有问题,JVM 仍然无法从 jar 包中查找正确的 class,原因是 hong.Person 必须按 hong/Person.class 存放,而不是 bin/hong/Person.class