本篇笔记主要介绍了 Java 的六个高级特性:异常处理、IO、泛型、集合框架、多线程以及 Lambda 表达式与流式处理。在异常处理部分,详细讲解了异常的分类、捕获与处理机制;IO 部分介绍了文本和二进制的输入输出操作;泛型部分阐述了泛型的使用规则与限制;集合框架部分系统地介绍了 Java 常用的集合类;多线程部分包含了线程的创建、控制与同步机制;最后的 Lambda 与流式处理部分则介绍了 Java 的函数式编程特性。(由 claude-3.5-sonnet 生成摘要)
1. Ch13 Exception Handling and Text IO
1.1. Exceptions
- 异常(exception) 是程序运行时发生的错误事件,可能导致程序无法正常运行。
- 常见的异常:
ArithmeticException
:除零错误。InputMismatchException
:输入数据类型不匹配。NullPointerException
:访问未初始化的对象。
- 异常分类:
- 上半部分的
Exception
是程序级错误,可以捕获并处理。- 运行时异常(
RuntimeException
)通常由逻辑错误引发,属于 非检查型异常(unchecked exception)。- 不需要强制捕获,通常由逻辑错误引发。
- 非运行时异常(如
IOException
等)需要显式处理,属于 检查型异常(checked exception)。- 编译器强制程序员检查并处理,即程序员应该使用使用
try-catch
块进行捕获这些异常并进行操作。
- 编译器强制程序员检查并处理,即程序员应该使用使用
- 运行时异常(
- 下半部分的
Error
是系统级错误,由 JVM 检测,通常不可恢复,比如内存不足。
- 上半部分的
- 抛出异常:用
throw
关键词抛出异常示例,异常示例通常使用new
运算进行创建。- 语法:
throw new TheException();
- 语法:
- 声明异常:定义方法时用
throws
关键词声明可能抛出的异常,多个异常之间用逗号隔开。- 使用这种方法声明的异常是检查型异常,必须在调用该方法的代码中进行显示捕获,否则会报 CE。
- 语法:
public void myMethod() throws IOException
- 调用时,必须用:
try { } catch(IOException ex) { }
来处理可能抛出的异常。
1.2. Exception Handling
- 用
try-catch
块捕获异常。- 语法:
try { } catch(ExceptionType ex) { }
- 语法:
finally
块:finally
块中的代码无论是否发生异常都会执行。- 使用
finally
块与在try-catch
块之后跟语句的区别时:如果在catch
块中抛出异常(或者 return 等情况),则try-catch
块之后的代码不会执行,但是finally
中的代码始终会执行。
finally
关键字保证无论程序使用何种方式退出try-catch
块,finally
块中的代码都会被执行。更具体的,finally
块会在以下情况发生之后执行:- try 块中的代码正常执行完毕。
- 在 try 块中抛出异常。
- 在 try 块中执行 return、break、continue。
- catch 块中代码执行完毕。
- 在 catch 块中抛出异常。
- 在 catch 块中执行 return、break、continue
- 通常使用
finally
块来释放资源。
- 多个
catch
块的情况:- 拥有多个
catch
块时,JVM 会由上而下来检测每个异常是否被捕获。 - 不可达代码检测机制:如果先
catch
了一个异常,又catch
了另一个异常的子类,则后者是不可达的。因为无论抛出什么异常,都会被先前的catch
块捕获。编译器会检测这种不可达的catch
块,并报 CE。- 所以捕获子类异常的
catch
块要放在捕获父类异常的catch
块之前。
- 所以捕获子类异常的
- 拥有多个
- 捕获到异常
e
后:- 使用
e.getMessage()
获取异常信息。 - 使用
e.printStackTrace()
打印异常调用堆栈。 - 如果需要进一步抛出异常,建议带上
e
的异常信息:可以使用Throwable(Throwable cause)
或者Throwable(String message, Throwable cause)
的构造方法。
- 使用
1.3. Assertions
- 断言(assertions):用于验证程序中的某些假设是否成立。
- 语法:
assert condition;
,其中condition
是一个布尔表达式。assert condition : message;
,其中message
可以是基本数据类型或对象。
- 当断言失败时,JVM 会抛出
AssertionError
异常。 - 设计原则:
- 断言仅用于开发阶段的内部检查。
- 不应在公共方法中使用断言检查参数。
- 断言默认在运行时禁用,需要在命令行手动使用
-enableassertions
或-ea
选项启用。
1.4. Text IO
File
类:提供文件和路径的抽象。- 常用方法:
exists()
:检查文件是否存在。createNewFile()
:创建新文件。delete()
:删除文件。
- 常用方法:
- 使用
Scanner
类读取数据。- 使用
new Scanner(Paths.get(path))
从文件中创建Scanner
对象,注意不能直接写字符串。 - 使用
new Scanner((new URL(url)).openStream())
从网络资源中创建Scanner
对象。 - 第二个参数(可选)用于指定编码,否则使用该系统的默认编码。
- 使用
- 使用
PrintWriter
类向文件写入数据。- 支持
print(...)
、println(...)
、printf(...)
等方法。 - 第二个参数(可选)用于指定编码,否则使用该系统的默认编码。
- 支持
2. Ch14 Binary IO
2.1. Binary IO Basic
- 几乎所有方法都需要显示捕捉
java.io.Exception
异常,故应显式声明或捕捉。 inputStream.read()
的返回值和outputStream.write(int b)
都是int
,但实际上都是以byte
为单位读写的。- 其中
inputStream.read()
在 EOF 时会返回 -1,这就是为什么需要用int
而不是byte
表示。
- 其中
DataInputStream
在到达文件末尾后继续读取数据会抛出EOFException
异常。Reader
是读取字符流的抽象类,Writer
是写入字符流的抽象类。InputStreamReader
:字节流转字符流OutputStreamWrite
:字符流转字节流BufferedReader
、BufferedWriter
:字符缓冲流FileReader
:InputStreamReader
类的直接子类,用来读取字符文件FileWriter
:OutputStreamWriter
类的直接子类,用来写入字符文件
2.2. Object IO Stream
transient
关键字:在序列化时忽略(静态成员也会被忽略)。- 如果一个对象不止一次写入对象流,不会存储对象的多份副本。JVM 会将对象的内容和序号一起写入对象流。
- 反序列化后的对象,不需要调用构造函数重新构造。
- 序列前的对象与序列化后的对象是深复制。
- 类需要显示地实现
Serializable
接口才能被序列化。(即使成员都可以序列化也需要这么做) - 如果数组中的所有元素都是可序列化的,这个数组就是可序列化的。
- 24-25 秋冬期末考试还考察了子类/父类中只有一个实现了序列化接口在序列化/反序列化时的行为,读者可以自行查阅相关资料。
2.3. Piped IO Stream
- PipedOutputStream 和 PipedInputStream 的作用是让多线程可以通过管道进行线程间的通讯。PipedReader 和 PipedWriter 和这两个的区别也类似:操作字符而不是字节。
- 我们在线程 A 中向 PipedOutputStream 中写入数据,这些数据会自动的发送到与 PipedOutputStream 对应的 PipedInputStream 中,进而存储在 PipedInputStream 的缓冲中;此时,线程 B 通过读取 PipedInputStream 中的数据。就可以实现,线程 A 和线程 B 的通信。
3. Ch16 Generics
- 构造方法中不需要写泛型(如是
Stack()
而不是Stack<E>()
) - 泛型方法若编译器可以推断则可省略,否则用类似
GenericMethodDemo.<Integer>print(integers)
的语法。 - 有界泛型类型(bounded generic type):
<E extends GeometricObject>
表示E
必须是GeometricObject
或其子类。 - 泛型类型之间没有继承关系
- 如
Integer
和Number
有继承关系但是GenericStack<Integer>
和GenericStack<Number>
没有 List<Integer> list = new List<Object>();
报错且反之亦然
- 如
- 三种通配
- unbound wildcard:
<?>
适用于任何类型 - bound wildcard:
<? extends T>
类型参数必须是 T 或其子类。List<? extends Number> list = new ArrayList<Integer>();
- 不能添加
Number
或Integer
或Double
到 list 中
- lower-bound wildcard:
<? super T>
表示类型参数必须是 T 或其父类。List<? super Integer> list = new ArrayList<Number>();
- 只能添加
Integer
或Integer
的子类到 list 中
- unbound wildcard:
- 泛型的限制
- 不能创建泛型类型的实例
new E()
会 CE
- 不能创建泛型数组
new E[10]
会 CE,应改为(E[]) new Object[10]
- 泛型类的静态上下文中不能使用类型参数
- 泛型类的所有实例都有相同的运行时类,所以泛型类的静态变量和方法是被它的所有实例共享的。
- 在静态方法、数据域或初始化语句中,为了类而引用泛型参数是非法的。
- 泛型类不能继承
Throwable
class MyException<T> extends Exception {}
会 CE- 主要是为了 catch 的问题
- 不能创建泛型类型的实例
ArrayList<String>
不是一个类,所以ArrayList<String> list1 = new ArrayList<String>();
后调用list1 instanceof ArrayList<String>
是错误的。
4. Ch17 Java Collections Framework
- Collection 接口关系图(P22)
Map
是独立的接口,不继承自Collection
ArrayList
和LinkedList
实现了List
接口。HashSet
和TreeSet
实现了Set
接口。HashMap
和TreeMap
实现了Map
接口。
addAll
、removeAll
、retainAll
类似于集合的并、差、交。- Set
HashSet
- 通过
hashCode
和equals
方法保证元素唯一性。 - 集合元素可以是 null,但只能放入一个 null
- 通过
LinkedHashSet
- 额外使用双向链表维护元素的插入顺序(似乎访问也还会被提到最前,即遵循 LRU)。
TreeSet
TreeSet
是SortedSet
接口的唯一实现类SortedSet
是 Set 的一个子接口。first()和 last()返回集合中的第一个和最后一个元素;headSet(toElement)和 tailSet(fromElement)返回集合中元素小于 toElement 和大于 fromElement 的那部分。- NavigableSet 扩展了 SortedSet,提供导航方法 lower(e),floor(e), ceiling(e)和 higher(e),分别返回小于、小于或等于、大于或等于、大于一个元素的元素,若没这样的元素,则返回 null。
- pollFirst()和 pollLast()则分别删除和返回 TreeSet 中的第一个和最后一个元素。
- List
- ArrayList:基于动态数组。
- 支持随机访问,访问速度快。插入和删除操作效率低(特别是中间位置)。
- LinkedList:基于双向链表。
- 插入和删除速度快。查询效率较低。
- ArrayList:基于动态数组。
- Map:
HashMap
、TreeMap
、LinkedHashMap
。- 对于
HashMap
和LinkedHashMap
,自定义对象作为 key 时需要重写hashcode()
和equals()
方法
- 对于
- ArrayList 中迭代器的陷阱
- 在迭代过程中,调用容器的删除方法,则会抛出异常
- 迭代器内部会维护索引位置相关的数据(包含最近返回的元素的索引和下一个返回的元素的索引),在迭代过程中,容器不能发生结构性变化
List<String> a = new ArrayList<String>();
和List<String> a = new ArrayList<>();
都是合法的写法。Map
不能直接使用Iterator
遍历,但可以通过entrySet()
、keySet()
或values()
方法间接获取可迭代的集合。for (Map.Entry<String, Integer> entry : map.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); }
5. Ch25 Multithreading
- 创建线程
- 实现
Runnable
接口并将实例传递给Thread
构造函数 - 继承
Thread
类:重写run()
方法
- 实现
- 线程控制
- 使用
run()
启动在当前线程创建,使用start()
启动在新线程创建 Thread.yield()
:让出当前线程的 CPU 时间片。Thread.sleep(long millis)
静态方法:当前线程休眠指定时间。anotherThread.join()
:等待另一个线程结束。- 中断
- InterruptedException:是 sleep 和 join 的必检异常,注意放在循环体中要手动 break。
- 线程的生命周期
- New(新建):线程对象被创建,但未调用
start()
方法。 - Runnable(就绪):调用
start()
方法后,线程处于可运行状态,但可能未获得 CPU 时间。 - Running(运行):线程获得 CPU 时间,正在执行任务。
- Blocked(阻塞):线程等待某些资源(如锁或 IO)。
- Terminated(终止):线程执行完毕或被中断。
- New(新建):线程对象被创建,但未调用
isAlive()
:用于检查线程是否处于活动状态(Ready、Blocked 或 Running)interrupt()
:设置中断标志。对于受阻塞(Object.wait()
、Thread.join()
、Thread.sleep()
)的线程,将其唤醒并抛出 InterruptedException;否则将暂时不起作用。isInterrupted()
:检查线程的中断标志是否被设置。不会清除中断标志。
- 优先级控制
setPriority(int newPriority)
:设置线程的优先级。Thread.MAX_PRIORITY
/Thread.MIN_PRIORITY
/Thread.NORM_PRIORITY
- 使用
- 线程同步(thread synchronization)
- 内存可见性问题:一个线程对共享变量的修改,可能不被另一个线程及时看到。因为每个线程都有自己的工作内存,而共享变量存储在主内存中。如果线程对共享变量的修改没有及时刷新到主内存就可能出现问题。
volatile
关键词:保证变量的可见性。synchronized
关键词或synchronized
块- 成员方法加
synchronized
相当于对this
上锁,静态方法加synchronized
相当于对XXClass.class
上锁。 - 功能
- 保证可见性:释放锁时,所有的写入都会写回内存,获得锁后,会从内存中读最新数据。
- 保证互斥性:同一时刻只有一个线程可以执行 synchronized 块中的代码。
- 可重入性:对同一个线程,它在获得锁之后,在调用其他需要同样锁的代码时,可以直接调用。
- 成员方法加
wait()
¬ify()
- 必须在
synchronized
方法或代码块中使用。 - 用法
wait()
:当前线程等待,释放锁。notify()
:唤醒一个等待线程。notifyAll()
:唤醒所有等待线程。
- 机制
- 把当前线程放入条件等待队列,释放对象锁,阻塞等待,线程状态变为 WAITING 或 TIMED_WAITING。
- 等待时间到或被其他线程调用 notify/notifyAll 从条件队列中移除,这是,要重新竞争对象锁
- 如果能获得锁,线程变为 RUNNABLE,并从 wait 调用中返回
- 否则,线程加入对象锁等待队列,线程变为 BLOCKED,只有在获得锁后才会从 wait 调用中返回。
- 必须在
6. Ch28 Lambda & Stream & RTTI
- Lambda 表达式是一种特殊的匿名内部类。
- 任何可以接受一个函数式接口(Functional Interface, FI)实例的地方,都可以使用 Lambda 表达式。
- 函数式接口:只能有一个抽象方法。可以有多个静态方法和默认方法。接口中的方法默认是
public abstract
。java.util.function
包中包含了多种函数式接口,例如:Function
:接收参数并返回结果。Predicate
:接收参数并返回布尔值。Consumer
:接收参数但无返回值。Supplier
:无参数但返回结果。
- Stream 对象:实现了
java.util.stream.Stream
接口。- 生成流:
stream()
或parallelStream()
或Stream.of("a1", "a2", "a3")
- 常用方法:
- filter:过滤满足条件的元素。
- map:映射为新值
- flatMap:映射成流并 flat 成一个
- distinct:去重(基于
hashCode
和equals
) - sorted:排序
- limit(int n):只保留前至多 个
- skip(int n):跳过前 个
- 终端操作:
boolean allMatch(Predicate<? super T> predicate);
检查是否匹配所有元素。boolean anyMatch(Predicate<? super T> predicate);
检查是否至少匹配一个元素。boolean noneMatch(Predicate<? super T> predicate);
检查是否没有匹配所有元素。Optional<T> findFirst();
返回当前流中的第一个元素。Optional<T> findAny();
返回当前流中的任意元素。long count();
返回流中元素总数。Optional<T> max(Comparator<? super T> comparator);
返回流中最大值。Optional<T> min(Comparator<? super T> comparator);
返回流中最小值。T reduce(T identity, BinaryOperator<T> accumulator);
可以将流中元素反复结合起来,得到一个值。返回 T。这是一个归约操作。collect
:转化为其他形式,如.collect(Collectors.toSet())
、toList()
- 生成流:
IntStream
与getAsInt()
- RTTI
object.getClass()
或TheClass.class
来获取类对象。clazz.isInstance(obj)
判断obj
是不是当前类或当前类子类的实例getSuperclass()
、getInterfaces()
、getModifiers()
- Method 类
- invoke()
- instanceof 运算符