Java高级特性

参考文献

  • JVM: Java Virtual Machine
  • JRE: Java Runtime Environment
  • JDK: Java Development Kit

面向对象,多态

多态就是“同一个行为,不同对象有不同的实现方式”。对于同一个方法,在同一父类下的不同子类都会有不同的反应。编译时,编译器只认某个变量的类型为其父类,只要父类中有这个方法,代码就能通过。而在运行时,jvm会查看变量指向的对象实际是什么,再调用真正的方法。这叫“动态绑定”或“运行时多态”。而在重写方法时,子类会复制一份父类的方法到自己的虚方法中。在调用方法时,若子类实现了该方法,则直接调用;反之调用指针指向的父类方法。

以下是几个面向对象的设计原则:
里氏代换原则:父类能出现的地方子类也一定能出现
单一职责原则:一个类只负责一项职责
开闭原则:软件实体就扩展开放,对修改关闭(添加新功能时应该增加代码而非修改原有代码)
接口隔离原则:客户端不应该依赖它不需要的接口
依赖倒置原则:高层模块不应该依赖于底层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象

equals和hashCode

为什么重写equals的时候必须也重写hashCode?hashCode方法用于获取哈希码,返回一个int整数。该整数多用于存放hashMap中的key。只重写了equals方法可能会导致hashMap不符合预期的问题。哈希码可能会一致,hashMap在处理键时,不仅会比较键对象的哈希码,还会使用equals方法检查键对象是否真正相等。

String,StringBuilder和StringBuffer

String对象本身是不可变的,每次对String对象进行修改操作时(比如加号操作),实质上都会先生成一个StringBuilder对象执行.append()操作,再使用.toString()方法换上去。这可能会导致不必要的内存和性能开销。因此在需要操作大量字符串的情况下,可以采用StringBuilder类实现。StringBuilder提供了一系列方法进行字符串的增删改查操作,这些操作都是在字符串原有的字符数组上实现的,不会产生新的String对象。StringBufferStringBuilder的线程安全版,方法前面都加有synchronized关键字。但真有人在并发状态下编辑字符串吗?

使用String s = "abc"(字面量方式)创建新的String对象时,jvm会首先检查字符串常量池中是否会存在"abc",若存在则直接返回常量池中的引用,否则则在常量池中创建"abc"对象,然后返回其引用。
而使用String s = new String("abc")new关键字方式)创建新的String对象时,除了在常量池进行类似操作外,还会在堆内存中创建一个新的String对象,并且往后都会指向堆内存中的新对象。
因此一般推荐使用字面量方式来初始化字符串。如果想使用new关键字方式实现类似字面量方式的效果,需要加.intern()方法,即String s = new String("abc").intern()

String的不可变性使得String对象在使用时更加安全,更容易缓存和重用,并且能作为某些集合类的键。为了保证其不可变性,该类有final声明,防止被继承;字符数组成员定义为private final char value[];每一次调用方法实际上都返回一个新的对象;每次构造时都会传递一份拷贝而非参数本身。

String转换为Integer有两种方法,一种是Integer.parseInt(String s),一种是Integer.valueOf(String s),但两种最终都会调用Integer.perseInt(String s, int radix)方法,其中是简单的字符串遍历算法。

包装类缓存机制

对基本类型的包装类,Java提供了缓存机制

包装类 缓存范围 备注
Integer -128~127 默认范围
Byte -128~127 全部缓存
Short -128~127 默认范围
Long -128~127 默认范围
Character 0~127 ASCII字符范围
Boolean true false 都缓存

可以在运行时添加 -Djava.lang.Integer.IntegerCache.high = 10000来调整缓存的最大值为10000。

因此对于如下语句

1
2
3
Integer a = 127;
Integer b = 127;
return a == b;

会返回true,因为在缓存区内,对象引用都是缓存区里的对象。而对于如下语句

1
2
3
Integer a = 128;
Integer b = 128;
return a == b;

会返回false,因此不再缓存区内,这是两个不同的对象。若需要判断值是否相等,则最好调用.equals()方法。

总之,基本类型的包装类会部分或全部先缓存好,在新建包装类时若基本类的值在缓存内,jvm直接返回其缓存中的地址。

Object类中的基本方法

Object类是所有类的父类,其中定义了所有对象都能用的方法。

  • hashCode()
    返回对象的哈希码。若重写了equals()方法,则必须要重写hashCode()方法。
  • equals()
    比较二者的内存地址是否相等(对象名本身存储着的就是地址),若需要通过比较其他部分来判断则需要重写。
  • clone()
    返回某个对象的浅拷贝,需要该对象实现Cloneable接口,否则会报CloneNotSupportedException错误。

    浅拷贝只拷贝本身,深拷贝递归地拷贝其子类。

  • toString()
    实现对象转字符串,默认返回类名@哈希码的十六进制表示,若要返回其他类型的字符串则需要重写。该方法也可以交给Lombok的@Data注解自动生成。而数组也是一个对象,直接调用toString()方法也会看到哈希码结果。
  • wait() notify()
    多线程下的线程等待和唤醒。
  • getClass()
    返回对象的类信息,返回值的类型为Class<?>,在反射中常用。
  • finalize()
    垃圾回收,若需在回收时自定义则调用次方法,但在Java 9已弃用。

对于异常的处理机制,此处不单开一章了。
若在try块中returnfinally块会先执行再返回。若finally也有return,则返回finally中的结果。而在这一块代码中

1
2
3
4
5
6
7
8
9
public static int test() {
int i = 0;
try {
i = 2;
return i;
} finally {
i = 3;
}
}

会返回2而不是3。在try块开始return时,不是立即返回,而是先算出需要return的值,然后将该值进行缓存,然后跳转到finally块。此时的finally改变的仅是局部变量的值而非缓存里的值,因此无法改变其缓存。

IO流

流的分类方式有很多种。可以按照其流动方向将其分为输入流输出流;按照其数据单位分为处理音频、图像等二进制数据的字节流,和处理文本数据的字符流;按功能划分为与输入输出端相连的节点流,对已存在的流进行包装的缓冲流,进行线程间的数据传输的管道流

流的分类

当生产消费速度不匹配,数据写入速度大于读取/处理速度时(比如客户端发送数据过快,文件写入速度过快,控制台输入数据过多等),可能会导致缓冲区无法容纳更多数据而出现异常或数据丢失的问题。为了避免此类问题,可以合理设置缓冲区大小,或者控制写入的数据量,进行流量控制,或者使用NIO的非阻塞IO。

字节流是由jvm将字节转换得到的,该过程比较耗时且容易出现乱码问题,因此IO流也提供了直接操作字符的字符流。在大文本中查找某个字符串时更推荐使用字符流,对视频文件通常使用字节流,并且尽量使用缓冲流来提高读写速度。

Java常见的IO模型有3种:BIO、NIO、AIO。

  • BIO(Blocking IO)阻塞式IO,使用字节流或字符流,线程在执行IO操作时被阻塞,无法执行其他任务。适用于连接数较少且固定的架构,如传统HTTP服务器。
  • NIO(Non-Blocking IO)同步非阻塞IO,使用多路复用器,轮询多个通道,只有就绪的通道才进行IO操作,线程在等待期间可以做其他事,通过轮询检查状态。使用于高并发场景,是目前的主流。
  • AIO(Asynchronous IO)异步非阻塞IO,使用回调函数或Future,操作完成后IO主动通知。线程发起请求后立即返回,完全不等。适用于连接数多且连接时间长(如大型文件读写,数据库连接池等)的场景。

普通的对象要想转换成流,需要先进行序列化。序列化是指将对象转换为字节流的过程,而反序列化是将字节流转换回对象的过程。若希望将某个对象序列化,需要使用Serializable接口进行标记。
serialVersionUID是Java序列化机制种用于标识类版本的唯一标识符,该标识符要求在序列化和反序列化中保持一致,用于在序列化和反序列化的过程中,类的版本是兼容的。该标识符可以自己设定,可以由IDEA自动生成,也可以交由Java自动生成。
Java序列化不包含静态变量。这是因为序列化机制只保存对象的状态,而静态变量属于类的状态。
可以用transient关键字修饰不像想被序列化的变量。

序列化一般包括以下3个过程:先实现Serializable接口,然后使用ObjectOutputStream来将对象写入,最后调用其中的writeObject()方法,将对象序列化后写入输出流中。

序列化一般有3种方式:对象流序列化、json序列化、protoBuff序列化。一般使用的是json序列化,一般需要Jackson包,将对象转化为byte数组或String字符串。

Socket套接字

Socket是网络通信的基础,表示两台设备间通信的一个端点,Socket通常用于简历TCP或UDP链接,实现进程间的网络通信。
使用socket实现一个简单的TCP通信。

TCP客户端

1
2
3
4
5
6
7
8
9
10
11
12
class TcpClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 8080);

BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);

// ...

socket.close();
}
}

TCP服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class TcpServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
Socket socket = serverSocket.accept();

BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);

// ...

socket.close();
serverSocket.close();
}
}

RPC框架是一种协议,允许程序调用位于远程服务器上的方法,就像调用本地方法一样。RPC通常基于Socket通信实现。
RPC协议

常见的RPC框架包括

  1. gRPC:基于 HTTP/2 和 Protocol Buffers。
  2. Dubbo:阿里开源的分布式 RPC 框架,适合微服务场景。
  3. Spring Cloud OpenFeign:基于 REST 的轻量级 RPC 框架。
  4. Thrift:Apache 的跨语言 RPC 框架,支持多语言代码生成。

常用的泛型通配符为 ? T K/V E等。

Java的泛型是伪泛型,这是因为Java在编译期间,所有的类型信息都会被擦掉,也就是说,在运行的时候是没有泛型的。该特性主要是为了向下兼容,因为jdk5之前是没有泛型的。

注解的生命周期有三大类,分别是:

  • RetentionPolicy.SOURCE: 源码级别,给编译器用的,不会写入class文件。如@Override,Lombok的@Getter会在class文件中写入对应的getter,但注解本身不会保留。
  • RetentionPolicy.CLASS: 会写入class文件,在类加载阶段丢弃。这是定义注解时的默认配置。如一些AOP框架或字节码增强库。
  • RetentionPolicy.RUNTIME: 写入class文件,永久保存,并可以通过反射获取注解信息。如Spring框架中的@Controller等。

反射

反射允许Java在运行时检查和操作类的方法和字段。多用于Spring框架的加载和管理Bean和动态代理等。
Java程序的执行分为编译和运行两步。编译后会生成字节码文件,jvm进行类加载的时候,会加载这个字节码文件,将类型相关的所有信息加载进方法区,反射就是去获取这些信息。

可以通过该方法加载并实例化类

1
2
3
4
// 根据传入的完整类名的字符串,动态地加载这个类到jvm中。返回一个Class对象
Class<?> cls = Class.forName("java.util.Date");
// 按照上一步获取的CLass对象,新建该类的一个实例。
Object obj = cls.newInstance();

获取并调用方法

1
2
3
4
// 从Class对象中查找并获取一个特定的公共方法。
Method method = cls.getMethod("getTime");
// 调用上一步获得的方法。第一个参数指定在哪个对象上调用这个方法,后续的参数则是方法本身的参数。
Object result = method.invoke(obj);

访问字段

1
2
3
4
5
6
// 从Java对象中获取一个声明的字段
Field field = cls.getDeclaredField("fastTime");
// 突破访问权限限制,对于私有变量需要这么做
field.setAccessible(true);
// 获取该字段的值。知道其Long类型就getLong,不知道则为get
Long number = field.getLong(obj);

Lambda 表达式主要用于提供一种简洁的方式来表示匿名方法,使 Java 具备了函数式编程的特性。所谓的函数式编程,就是把函数作为参数传递给方法,或者作为方法的结果返回。

Optional是用于防范NullPointerException的工具。可以将Optional看作是一种包装对象的容器,当某方法的返回值可能是空的时候可以使用该容器包装。

Stream流

该特性可以对包含一个或多个元素的集合做各种操作,这些操作可能是中间操作或终端操作。中间操作能执行多次,而终端操作只能执行一次。

Stream的创建有多种方式。可以从集合创建,也可以从数组创建。
还可以使用Stream.of()方法显式声明。

1
Stream<String> stream = Stream.of("a", "b", "c");

还可以使用lambda函数显式声明

1
2
Stream<Integer> infiniteStream = Stream.iterate(0, n -> n + 2); // 无限偶数流
Stream<Double> randomStream = Stream.generate(Math::random); // 无限随机数流

常用中间操作如下

中间操作类别 方法 说明
过滤操作 filter() 进行过滤,参数放匿名函数
distinct() 去重,不放参数
limited() 限制元素数量,参数放整数
skip() 跳过前n个元素,参数放整数
映射操作 map() 一对一映射,将每个元素转换为另一个元素,然后将这些流合并为另一个流,参数是一个匿名函数
flatMap() 一对多映射,将所有生成的流片扁平化为一个流,参数是一个匿名函数
排序操作 sorted() 自然排序
sorted(Comparator.reverseOrder()) 自定义排序,这里是逆序

常用终端操作如下

终端操作类别 方法 说明
匹配操作
返回Boolean
allMatch() 是否全部匹配,参数为匿名函数
anyMatch() 是否有匹配的,参数为匿名函数
noneMatch() 是否没有匹配的,参数为匿名函数
查找操作 findFirst 返回第一个元素(如果有),没有参数
findAny() 在顺序流中返回第一个元素,在并行流中返回任意一个元素,没有参数
规约操作 reduce(0, Integer::sum) 求和操作,实例中为给装Integer的集合求和,第一个参数为求和前的初始值
第二个参数为一个匿名函数,放置一个累加器
count() 计数,没有参数
max()/min() 最大最小值, 参数为匿名函数。Integer中为Integer::compareTo
收集操作 collect(Collection.toList()) 收集为某一集合类,实例中为转化为List
collect(Collectors.joining(", ")) 按照特定的规则链接字符串,示例中为字符串间添加", "
collect(Collectors.groupingBy()) 按照特定的规则分组,参数为匿名函数,返回一个值为List的Map
collect(Collectors.partitioningBy()) 进行分区,将符合与不符合的元素分成两组,参数为匿名函数,返回值为值为List的Map

Java高级特性
https://ivanclf.github.io/2025/09/26/java/
作者
Ivan Chan
发布于
2025年9月26日
许可协议