JDK版本特性
Oracle官网https://www.oracle.com/java/technologies/java-se-support-roadmap.html
Oracle官网中JDK版本的说明,Java SE 8、11、17和21是LTS版本。也就是长期支持版本。
我们针对这几个版本了解学习下对应版本的新特性。
JDK8版本
正式发布于2014年。
比较重要的新增特性如下:
1、移除HotSpot虚拟机的永久代,使用元空间替代
永久代有固定的大小限制,在JDK8之前可通过JVM参数 -XX:MaxPermSize= xxx m 设置,当加载的类的大小超过这个限制时,就会抛出 OutOfMemoryError: PermGen space 错误。这种设计限制了 Java 的灵活性和可伸缩性。
元空间的本质和永久代类似,都是对JVM规范中方法区的实现。移除永久代主要是为了解决与类元数据存储相关的内存管理和性能问题。
元空间相比于永久代具有以下优势:
- 元空间基于本地内存不受 Java 堆内存大小的限制。
- 元空间的大小可以根据应用程序的需求动态调整,减少了内存溢出的风险,并允许应用更高效地管理内存。
2、Lambda 表达式
语法:
Lambda 表达式基本语法如下:
(参数) -> 表达式 或者 (参数) -> { 代码块; }
作用:
Lambda 表达式的主要作用是简化部分匿名内部类的写法。
特点:
- 参数无需声明类型: Lambda 表达式不需要声明参数类型,编译器可以自动推断参数类型。
- 参数括号可选: 当只有一个参数时,可以省略括号。当参数个数大于一个时,括号是必需的。空括号用于表示空参数集。
- 表达式的大括号可选: 当 Lambda 表达式的主体只包含一个表达式时,可以省略大括号。当表达式需要包含多个语句时,必需要使用大括号。
- return关键字可选: 当 Lambda 表达式主体只有一个表达式,且该表达式会自动返回结果时,可以省略 return 关键字。
Lambda 表达式用法的简单示例:
public static void main(String[] args) {
// ()表示空参数列表 且省略了 {}
Thread thread = new Thread(() -> System.out.println("Lambda表达式"));
Map<String, String> map = new HashMap<>();
map.put("123","123");
// (k,v) 表示参数列表 分别表示 Map的key的value
map.forEach((k,v)->{
System.out.println(k);
System.out.println(v);
});
// removeIf要求必需有返回值
List<String> list = new ArrayList<>();
list.removeIf((item)->{ return "1".equals(item);});
}
要理解Lambda 表达式的使用还需要了解一个知识点:函数式接口
3、函数式接口
函数式接口是**只包含一个抽象方法**的接口,它是在 Java 8 版本中引入的,其主要目的是支持函数式编程,有了函数式接口我们可以将函数作为参数传递、将函数作为返回值返回,同时也为使用 Lambda 表达式提供了支持。
标准的函数式接口使用@FunctionalInterface注解标注,例如:
@FunctionalInterface只是一个标记注解,可以让编译器检查接口是否符合函数式接口的定义。
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
对于无参数,无返回值的函数式接口的抽象方法, Lambda表达的用法如下:
public static void main(String[] args) {
// 之前我们对于内名内部类的用法
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("匿名内部类");
}
};
// Runnable中只有一个run方法 且无参,无返回值 使用Lambda表达式
Runnable runnable1 = ()->{
System.out.println("使用Lambda表达式");
};
}
从上面的代码可以看出 Lambda表达式本质上就是 函数式接口的实例。
可以把Lambda表达式理解为函数式接口实例化后的对象。
其他的函数式接口:
为了丰富Lambda表达式的应用场景,Java内置了许多函数式接口:
在JDK8之前,我们经常使用的函数式接口有:
java.lang.Runnable
java.util.concurrent.Callable
java.util.Comparator
java.lang.reflect.InvocationHandler
在 JDK8 中,新增的函数式接口在 java.util.function 包中:
下面举几个常见的例子:
java.util.function.Consumer:消费型接口
java.util.function.Function:函数型接口
java.util.function.Supplier:供给型接口
java.util.function.Predicate:断定型接口
这几个新增的函数式接口大大丰富了Lambda表达式的使用。
Consumer 消费型接口
顾名思义,消费性接口肯定是拿到消息并消费,所以该接口中的方法,接收一个参数,但是没有返回值。
@FunctionalInterface
public interface Consumer<T> {
// 这个是抽象方法
void accept(T t);
// 这个是 default 修饰的实现方法 (这个也是JDK8的新特性 允许接口中有实现方法但是必需使用default 修饰)
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}
}
Consumer 接口的用法:
Consumer 接口接收一个对象,可以在accept方法中对该对象进行操作,例如修改对象状态,打印该对象等。
该接口中的andThen方法接收一个Consumer对象,先执行调用者Consumer对象的accept方法,再执行传入的Consumer对象的accept方法。也就是一条消息可以被多个消费者依次消费处理。
public static void main(String[] args) {
Consumer consumer1 = (s) -> {
System.out.println(s);
};
consumer1.accept("消息1"); // 结果 消息1
Consumer c1 = (s) -> {
s = s + "c1";
System.out.println(s);
};
Consumer c2 = (s) -> {
s = s + "c2";
System.out.println(s);
};
c1.andThen(c2).accept("消息被处理");
// 结果 消息被处理c1
// 消息被处理c2
}
Function函数型接口
Function 代表一个函数。
该接口包含一个抽象方法 R apply(T t),接受一个参数 t(类型为 T),并返回一个结果(类型为 R)
还包含3个实现方法
compose、andThen、identity
@FunctionalInterface
public interface Function<T, R> {
// 接收一个参数t ,返回一个参数R
R apply(T t);
// 接收一个Function类型的参数,调用接收参数的apply方法,返回Function
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
// 接收一个Function类型的参数,先调用自己的apply方法,再调用接收参数的apply方法
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
// 给啥返回啥
static <T> Function<T, T> identity() {
return t -> t;
}
}
Function接口的用法:
由于Function接口的apply方法接收的参数和返回的参数类型都是自定义的,所以可以用来做数据处理,参数的类型转换。
根据提供的compose方法和andThen方法 还可以做方法的链式调用。
至于给啥返回啥的identity方法,看似无用,实则还是有用的。
public static void main(String[] args) {
Function f1 = (t)->{
// ============== 类型转换
return Integer.parseInt((String) t);
};
Object apply = f1.apply("2024");
System.out.println(apply);
Function f2 = (t)->{
// ============== 数据处理
return (String)t + "数据处理";
};
String apply1 = (String) f2.apply("2024");
System.out.println(apply1);
// ============== 函数链式调用
Function chain1 = (t)->{
System.out.println("调用函数1");
return (String) t + 1;
};
Function chain2 = (t)->{
System.out.println("调用函数2");
return (String) t + 2;
};
Function chain3 = (t)->{
System.out.println("调用函数3");
return (String) t + 3;
};
// andThen 相当于从外到内顺序调用
String apply2 = (String) chain1.andThen(chain2).andThen(chain3).apply("0");
System.out.println(apply2);
// compose 相当于从内到外倒序调用
String apply3 = (String) chain1.compose(chain2).compose(chain3).apply("0");
System.out.println(apply3);
}
identity方法的妙用:
public static void main(String[] args) {
// ============ identity恒等函数的用法
ArrayList<Dog> dogs = new ArrayList<>();
dogs.add(new Dog("秀逗", "吃骨头"));
dogs.add(new Dog("四眼", "吃馒头"));
// 使用stream流 转map
// Dog::getName 方法引用 也是JDK8的新特性
// Function.identity() 我们想收集的map的value正好就是原对象
Map<String, Dog> dogMap = dogs.stream().
collect(Collectors.toMap(Dog::getName, Function.identity(), (k1, k2) -> k1));
System.out.println(dogMap);
}
class Dog {
private String name;
private String hobby;
public Dog(String name, String hobby) {
this.name = name;
this.hobby = hobby;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", hobby='" + hobby + '\'' +
'}';
}
}
Supplier供给型接口
Supplier 有一个 get() 抽象方法,用于获取结果。不接收参数,返回一个结果。
@FunctionalInterface
public interface Supplier<T> {
T get();
}
Supplier 可以用于延迟加载。
代码示例:
public static void main(String[] args) {
Supplier supplier = () -> {
return "Result";
};
// 只有调用get方法时 才会真正的获取结果
System.out.println(supplier.get());
}
Predicate断定型接口
Predicate接口中有个抽象方法test接收一个参数并返回一个布尔值。用于判断参数是否满足某种条件。
@FunctionalInterface
public interface Predicate<T> {
// 接收一个参数t 返回一个布尔值
boolean test(T t);
// 接收到一个 Predicate 类型的参数,先调用调用者自身的test方法再&&传入参数的test方法的结果并返回布尔值
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
// 调用test方法并取反
default Predicate<T> negate() {
return (t) -> !test(t);
}
// 接收一个 Predicate 类型的参数,先调用调用者自身的test方法再 || 传入参数的test方法的结果并返回布尔值
default Predicate<T> or(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) || other.test(t);
}
// 创建了一个 Predicate,它可以用来检查其他对象是否与给定的 targetRef 相等(如果 targetRef 非空)或是否都为 null(如果 targetRef 为空)。
static <T> Predicate<T> isEqual(Object targetRef) {
return (null == targetRef)
? Objects::isNull
: object -> targetRef.equals(object);
}
}
Predicate接口的用法:
public static void main(String[] args) {
Predicate predicate = (s)->{
if("哈士奇".equals(s)){
return true;
}
return false;
};
Predicate predicat1 = (s)->{
String str = (String) s;
if(str.length()==3){
return true;
}
return false;
};
boolean test = predicate.test("哈士奇");
System.out.println(test); // true
boolean test1 = predicate.and(predicat1).test("哈士奇");
System.out.println(test1); // true
Predicate<Object> equal = Predicate.isEqual("哈士奇");
boolean test2 = equal.test("二哈");
System.out.println(test2); // false
}
其他的函数式接口
就不一一介绍了,了解了上面四个 自己再去看JDK源码就比较好了解其他函数式接口用途了。
4、方法引用
方法引用通过提供一种更加简洁、清晰的方式来引用方法,不仅提升了代码的可读性和可维护性,简化了代码的编写。
方法引用类型:
类型 | 格式 |
---|---|
类的静态方法 | 类名::静态方法名 |
对象的实例方法 | 实例对象::方法名 |
类的实例方法 | 类名::实例方法名 |
类的构造器方法 | 类::new |
数组 | Type[]::new (Type表示数组的元素类型) |
如果使用IDEA编写程序,会有方法引用的提示。
代码示例:
public static void main(String[] args) {
// 类的静态方法引用
Runnable eat = Dog::eat;
eat.run();
// 对象的实例方法引用
Dog dog1 = new Dog();
Runnable drink1 = dog1::drink;
drink1.run();
// 类的实例方法引用
Consumer<Dog> drink = Dog::drink;
drink.accept(new Dog());
// 类的构造器方法引用
Supplier<Dog> dogSupplier = Dog::new;
Dog dog = dogSupplier.get();
// 数组引用
Function<Integer, int[]> function = int[]::new;
int[] arrays = function.apply(123);
}
class Dog {
private String name;
public static void eat() {
System.out.println("狗吃骨头!");
}
public void drink(){
System.out.println("狗喝水!");
}
}
5、Stream API
引入Stream API的目的是为了提高集合类型数据处理的效率和代码的简洁性。
Stream API的使用分为三步。
- ①、创建Stream流
- ②、对Stream进行中间操作
- ③、对Stream进行终端操作得到结果
创建Stream流
最常用的还是 通过集合的stream方法 创建Stream流
public static void main(String[] args) {
List<String> list = new ArrayList<>(Arrays.asList("1", "2", "3"));
// 通过集合的stream方法 创建Stream流
Stream<String> stream = list.stream();
// 通过 Arrays.stream方法 获取数组的 stream流
IntStream stream1 = Arrays.stream(new int[]{1, 2, 3});
// 使用 Stream.of方法
Stream<String> stream2 = Stream.of("1", "2", "3");
// 使用 Stream.builder方法
Stream.Builder<Object> builder = Stream.builder();
builder.accept("1");
builder.accept("2");
builder.accept("3");
Stream<Object> stream3 = builder.build();
}
中间操作
常用的中间操作:
方法名 | 描述 |
---|---|
map(Function<T, R> mapper) | 将元素通过给定的函数映射为另一个类型的元素 |
filter(Predicate predicate) | 根据给定的条件过滤元素 |
distinct() | 去除流中的重复元素 |
sorted() | 对元素进行排序,默认按自然顺序排序 |
sorted(Comparator comparator) | 使用自定义比较器对元素进行排序 |
limit(long maxSize) | 截取流中的前 maxSize 个元素 |
skip(long n) | 跳过流中的前N个元素 |
flatMap(Function<T, Stream> mapper) | 将每个元素映射为一个流,然后将这些流合并为一个流 |
中间操作代码示例:
public static void main(String[] args) {
List<Dog> list = new ArrayList<>();
Dog dog1 = new Dog("秀逗", 8);
Dog dog2 = new Dog("四眼", 4);
Dog dog3 = new Dog("四眼狗", 4);
Dog dog4 = new Dog("哈士奇", 4);
list.add(dog1);
list.add(dog2);
list.add(dog3);
list.add(dog4);
// ======================= 中间操作 =============================
// map中间操作 将Dog元素 映射成 Dog中的name元素
list.stream().map(Dog::getName);
// filter中间操作 根据给定的条件过滤元素
list.stream().filter((item) -> {
return item.getAge() > 4;
});
// distinct() 去重 去重的标准是基于对象的equals()和hashCode()方法
list.stream().distinct();
// sorted() 对元素进行排序,默认按自然顺序排序
list.stream().sorted();
// sorted(Comparator comparator) 使用自定义比较器对元素进行排序
// 先按照年龄倒序,再按照姓名正序
list.stream().sorted(Comparator.comparing(Dog::getAge).reversed()
.thenComparing(Dog::getName));
// 自定义 Comparator
Stream<Dog> sorted = list.stream().sorted((o1, o2) -> {
// 先比较年龄 按照年龄从小到大排序
int compareAge = Integer.compare(o1.getAge(), o2.getAge());
if (compareAge != 0) {
return compareAge;
}
// 再比较名字长度
int nameLength = Integer.compare(o1.getName().length(), o2.getName().length());
if (nameLength != 0) {
// 长度长的在前面
return -nameLength;
}
// 再自定义一个 如果名字里包含 哈士奇就排在前面
boolean b1 = o1.getName().contains("哈士奇");
boolean b2 = o2.getName().contains("哈士奇");
// b1包含哈士奇 b1排在前面
// 返回负数(如-1),表示第一个参数(在本例中是o1)应该排在第二个参数(o2)之前。
//返回正数(如1),表示第一个参数应该排在第二个参数之后。
//返回0,表示两者相等,对于排序而言,它们的相对位置不变
if (b1 && !b2) {
return -1;
}
// b2包含哈士奇 b2排在前面
if (!b1 && b2) {
return 1;
}
return 0;
});
// limit(n) 截取前n个元素
list.stream().limit(2);
// skip(long n) 跳过前n个元素
list.stream().skip(2);
// flatMap(Function<T, Stream> mapper) 将每个元素映射为一个流,然后将这些流合并为一个流
// 相当于把有层级的 对象 都扁平化 消除层级
List<List<Dog>> arrayLists = new ArrayList<>();
arrayLists.add(new ArrayList<>(Arrays.asList(dog1,dog2,dog3)));
arrayLists.add(new ArrayList<>(Arrays.asList(dog4)));
arrayLists.stream().flatMap(List::stream);
}
终端操作
方法名 | 描述 |
---|---|
collect() | 将流中的元素收集到集合或映射中,可以指定收集器来定制收集行为 |
forEach() | 对流中的每个元素执行指定的操作 |
forEachOrdered() | 与forEach类似,当使用parallelStream(并行流时)遍历时保留了元素的顺序 |
toArray() | 将流中的元素收集到数组中 |
reduce(accumulator) | 通过累积操作将流中的元素归约为单个结果 |
reduce(identity, accumulator) | 使用初始值和累积操作将流中的元素归约为单个结果 |
reduce(identity, accumulator, combiner) | 使用初始值、累积操作和组合操作将流中的元素归约为单个结果 |
min(comparator) | 使用指定的比较器找到流中的最小元素 |
max(comparator) | 使用指定的比较器找到流中的最大元素 |
count() | 计算流中元素的数量 |
anyMatch() | 检查流中是否有任何元素匹配指定的条件 |
allMatch() | 检查流中的所有元素是否都匹配指定的条件 |
noneMatch() | 检查流中是否没有元素匹配指定的条件 |
findFirst() | 返回流中的第一个元素(如果存在),通常与filter操作一起使用 |
findAny() | 返回流中的全部符合条件的元素(如果存在),通常与filter操作一起使用 |
终端操作代码示例:
public static void main(String[] args) {
Dog dog1 = new Dog("秀逗", 8);
Dog dog2 = new Dog("四眼", 4);
ArrayList<Dog> dogs = new ArrayList<>();
dogs.add(dog1);
dogs.add(dog2);
// =============== 终端操作 ===================
// ============= collect() 将流中的元素收集到集合或映射中,可以指定收集器来定制收集行为
// 收集Set
Set<String> dogSet = dogs.stream().map(Dog::getName).collect(Collectors.toSet());
// 收集List
List<String> dogList = dogs.stream().map(Dog::getName).collect(Collectors.toList());
// 收集Map
Map<String, Dog> dogMap = dogs.stream().collect(Collectors.toMap(Dog::getName, Function.identity(), (k1, k2) -> k1));
// 收集分组
Map<String, List<Dog>> dogGroup = dogs.stream().collect(Collectors.groupingBy(Dog::getName));
// ========== forEach 对流中的每个元素执行指定的操作
dogs.forEach(System.out::println);
// ========== forEachOrdered 与forEach类似,当使用parallelStream(并行流时)遍历时保留了元素的顺序
dogs.parallelStream().forEachOrdered(System.out::println);
// ========== toArray 将流中的元素收集到数组中
dogs.toArray();
// ========== reduce(accumulator) 通过累积操作将流中的元素归约为单个结果
List<String> list = Arrays.asList("1", "2", "3");
list.stream().reduce((a,b)-> a+b);
// ========== reduce(identity, accumulator) 使用初始值和累积操作将流中的元素归约为单个结果
dogs.stream().mapToInt(Dog::getAge).reduce(0, Integer::sum);
// ========== reduce(identity, accumulator, combiner) 使用初始值、累积操作和组合操作将流中的元素归约为单个结果
dogs.parallelStream()
.map(Dog::getAge) // 将Dog流转换为年龄的IntStream
.reduce(0, // 初始值 identity
Integer::sum, // 累积器 accumulator,直接使用Integer的sum方法进行累加
Integer::sum // 组合器 combiner,也是sum方法,用于并行处理时合并子结果
);
// ========== min(comparator) 使用指定的比较器找到流中的最小元素
dogs.stream().min(Comparator.comparing(Dog::getAge));
// ========== max(comparator) 使用指定的比较器找到流中的最大元素
dogs.stream().max(Comparator.comparing(Dog::getAge));
// ========== count() 计算流中元素的数量
dogs.stream().count();
// ========== anyMatch() 检查流中是否有任何元素匹配指定的条件|
dogs.stream().anyMatch((item)->{return item.getName().contains("哈士奇");});
// ========== allMatch() 检查流中的所有元素是否都匹配指定的条件
dogs.stream().allMatch((item)->{return item.getName().contains("哈士奇");});
// ========== noneMatch() 检查流中是否没有元素匹配指定的条件
dogs.stream().noneMatch((item)->{return item.getName().contains("哈士奇");});
// ============ findFirst 返回流中的第一个元素(如果存在),通常与filter操作一起使用
dogs.stream().filter((item)->{return item.getName().contains("哈士奇");}).findFirst();
// ============ findAny 返回流中的全部符合条件的元素(如果存在),通常与filter操作一起使用
dogs.stream().filter((item)->{return item.getName().contains("哈士奇");}).findAny();
}
6、Optional 类
Optional 类本质上是一个容器类,用来更优雅的处理可能出现的空指针问题。
Optional 使用建议:
-
避免将Optional用作方法的参数或返回类型: Optional应该用于表示可能为null的值,而不是作为方法的参数或返回类型。在方法签名中使用Optional可能会使代码变得复杂,并且不符合Optional的设计初衷。
-
使用isPresent()和ifPresent()进行判断: 在使用Optional时,最好使用isPresent()方法来检查Optional是否包含值,然后使用ifPresent()方法来执行相应的操作。这样可以避免使用get()方法,从而避免潜在的空指针异常。
-
避免滥用Optional: Optional并不是解决所有空指针问题的银弹。在某些情况下,使用传统的null检查可能更加简单和清晰。Optional应该用于表示可能为null的值,并且仅在需要对null进行特殊处理时使用。
-
使用orElse()和orElseGet()提供默认值: 在使用Optional时,可以使用orElse()方法或orElseGet()方法来提供一个默认值,以防Optional为空。orElse()方法在Optional为空时会返回默认值,而orElseGet()方法在Optional为空时会执行一个Supplier函数来生成默认值。
代码示例:
public static void main(String[] args) {
Dog dog = new Dog("Buddy", 3);
// 创建一个Optional实例
Optional<Dog> optionalDog = Optional.ofNullable(dog); // 确定有值时使用of,否则使用empty或ofNullable
// 1. isPresent() 和 get()
if (optionalDog.isPresent()) {
System.out.println("Dog's name: " + optionalDog.get().getName());
}
// 2. ifPresent(Consumer)
optionalDog.ifPresent(d -> System.out.println("Dog's age: " + d.getAge()));
// 3. map(Function)
Optional<String> dogNameOpt = optionalDog.map(Dog::getName);
dogNameOpt.ifPresent(System.out::println); // 输出狗的名字
// 4. flatMap(Function)
// 假设有个方法返回Optional<Integer>
Optional<String> mappedAgeOpt = optionalDog.flatMap(Dog::getOptionalName);
mappedAgeOpt.ifPresent(name -> System.out.println("Mapped name: " + name));
// 5. orElse(T)
String dogNameOrDefault = optionalDog.map(Dog::getName).orElse("Unknown");
System.out.println("Dog name or default: " + dogNameOrDefault);
// 6. orElseGet(Supplier)
String dogNameOrDefaultGet = optionalDog.map(Dog::getName).orElseGet(() -> "Fallback");
System.out.println("Dog name or default via Supplier: " + dogNameOrDefaultGet);
// 7. orElseThrow(Supplier)
try {
Dog dog1 = (Dog)Optional.empty().orElseThrow(() -> new IllegalArgumentException("Dog is missing"));
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
}
}
@Data
class Dog {
private String name;
private Integer age;
public Dog(String name, Integer age) {
this.name = name;
this.age = age;
}
public Optional<String> getOptionalName() {
return Optional.ofNullable(name);
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
7、LocalDate、LocalDateTime和DateTimeFormatter
在JDK8之前 处理日期、时间和格式化日期时间,使用 java.util.Date 、java.util.Calendar、java.text.SimpleDateFormat 这三个类。
为什么JDK8要引入 LocalDateTime 和 DateTimeFormatter呢?
有以下几个原因:
-
①、线程不安全:
java.util.Date 和 java.util.Calendar 线程不安全,这就导致我们在多线程环境使用需要额外注意。且java.util.Date类是可变的,我们可以随时修改它,如果不小心就会导致数据不一致问题
java.text.SimpleDateFormat 也是线程不安全的,这可能导致日期格式化错误。而且它的模式字符串容易出错,不够直观。 -
②、时区处理问题: Java 8 版本以前的日期 API 在时区处理上存在问题,例如时区转换和夏令时处理不够灵活和准确。而且时区信息在 Date 对象中存储不明确,这使得正确处理时区变得复杂。
LocalDate、LocalDateTime和DateTimeFormatter的使用示例:
public static void main(String[] args) {
// 获取当前时间
LocalDateTime now = LocalDateTime.now();
// 自定义格式化器
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String format = formatter.format(now);
System.out.println(format);
// 通过格式化器 解析字符串 日期时间
LocalDateTime parse1 = LocalDateTime.parse("2024-05-20 12:00:00", formatter);
System.out.println(parse1);
LocalDate date = LocalDate.now();
// 自定义格式化器
DateTimeFormatter formatterDate = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String formatDateStr = formatterDate.format(date);
System.out.println(formatDateStr);
// 通过格式化器 解析字符串 日期时间
LocalDate date1 = LocalDate.parse("2024-05-20", formatterDate);
System.out.println(date1);
}
使用DateTimeFormatterBuilder构造特殊的解析器
构建一个能够解析两种格式的日期解析器 yyyy-MM-dd 和 yyyy/MM/dd
public static void main(String[] args) {
// 构建一个能够解析两种格式的日期解析器
// DateTimeFormatterBuilder 使用了方括号 [ ] 来定义备选的格式模式。
// 这样构建的解析器会尝试按照提供的顺序匹配每一种模式,直到成功解析为止
DateTimeFormatter formatter = new DateTimeFormatterBuilder()
.appendPattern("[yyyy-MM-dd]")
.appendPattern("[yyyy/MM/dd]")
.toFormatter();
String dateStr1 = "2024-05-27";
String dateStr2 = "2024/05/27";
try {
LocalDate date1 = LocalDate.parse(dateStr1, formatter);
System.out.println("Parsed date 1: " + date1);
LocalDate date2 = LocalDate.parse(dateStr2, formatter);
System.out.println("Parsed date 2: " + date2);
} catch (DateTimeParseException e) {
System.err.println("Error parsing date: " + e.getMessage());
}
}
JDK11版本
挑几个比较重要的或者好用的特性学习下
1、全新的 HTTP 客户端 API
全新的 HTTP 客户端 API 优势:
- **①、HTTP/2支持:**全新的 HttpClient 支持 HTTP/2 协议的所有特性,包括同步和异步编程模型。
- ②、WebSocket支持: 支持WebSocket,允许建立持久的连接,并进行全双工通信。
- ③、同步和异步: 提供了同步和异步的两种模式。
- ④、链式调用: 新的HttpClient 允许链式调用,使得构建和发送请求变得更简单。
- ⑤、更好的错误处理机制: 新的HttpClient 提供了更好的错误处理机制,当HTTP请求失败时,可以通过异常机制更清晰地了解到发生了什么。
HTTP 客户端 核心类:
java.net.http.HttpClient
java.net.http.HttpRequest
java.net.http.HttpResponse
HttpClient类常用方法:
方法 | 注释 |
---|---|
connectTimeout() | 设置建立HTTP连接的超时时间 |
version() | 指定HTTP协议的版本(HTTP/1.1或HTTP/2) |
executor() | 设置自定义的Executor,该Executor用于异步任务 |
authenticator() | 设置HTTP身份验证 |
sslParameters() | 设置SSL/TLS的配置 |
HttpRequest类常用方法:
方法 | 注释 |
---|---|
uri() | 设置统一资源标识符 (URI) |
header() | 单个添加请求头 |
headers() | 批量添加请求头 |
setHeader() | 设置特定的请求头 |
GET() | 创建一个 GET 请求 |
POST(HttpRequest.BodyPublisher body) | 创建一个带有请求体的 POST 请求 |
PUT(HttpRequest.BodyPublisher body) | 创建一个带有请求体的 PUT 请求 |
DELETE() | 创建一个 DELETE请求 |
HttpResponse类常用方法:
方法 | 注释 |
---|---|
statusCode() | 获取HTTP响应的状态码 |
body() | 获取HTTP响应体 |
headers() | 获取HTTP响应头 |
同步请求使用示例:
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public class TestA {
public static void main(String[] args) {
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10)) // 连接超时为10秒
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://www.baidu.com")) // 链接
.GET() // 请求方式
.timeout(Duration.ofSeconds(10)) // 接口超时时间
.build();
HttpResponse<String> response = null;
try {
response = client.send(request, HttpResponse.BodyHandlers.ofString());
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(response.body());
}
}
异步请求使用示例:
public static void main(String[] args) {
// 构建 HttpClient 对象
HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10)) // 连接超时为10秒
.build();
// 构建 HttpRequest 对象
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://www.baidu.com")) // 链接
.timeout(Duration.ofSeconds(5)) // 超时时间为 5 秒
.GET() // 请求方式
.build();
// 发送异步请求
CompletableFuture<HttpResponse<String>> futureResponse = httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString());
// 处理响应
futureResponse.thenApply(HttpResponse::body) // 获取响应体
.thenAccept(System.out::println); // 打印响应体
System.out.println("异步请求");
futureResponse.join(); // 等待处理完成
}
2、处理文本文件的API
这两个API对于处理文本文件非常好用。
Files.readString(Path path):读取一个文件的所有内容并将其返回为一个字符串。
Files.writeString(Path path, CharSequence csq, OpenOption… options):将一个字符串写入到文件中。
代码示例:
public class TestA {
public static void main(String[] args) throws IOException {
Path path = Path.of("C:\\Users\\Administrator\\Desktop\\123.txt");
String s = Files.readString(path);
System.out.println(s);
String ss = "123231\nwqedqwew\nqweqwewq";
/**
* StandardOpenOption的值 默认是 CREATE 和 WRITE
*
* READ, 允许文件通道进行读取操作
* WRITE, 允许文件通道进行写入操作
* APPEND, 指示应该在文件的当前结尾之后追加数据,如果文件不存在,则创建新文件。
* TRUNCATE_EXISTING, 如果文件已经存在,将其长度截断为0,即清空文件内容,然后开始写入。
* CREATE, 如果指定的文件不存在,则创建它。如果文件已存在,这个选项本身不会影响其内容。
* CREATE_NEW, 仅在文件不存在时创建新文件。如果文件已存在,则写入操作失败并抛出FileAlreadyExistsException异常。
* DELETE_ON_CLOSE, 当关闭相关的通道或流时,自动删除此文件。这对于临时文件特别有用。
* SPARSE, 指示文件系统应为文件使用稀疏文件支持。这对于大文件特别有用,可以减少实际分配的磁盘空间。
* SYNC, 要求对文件的元数据和内容的更新同步写入到存储设备。这是最安全但可能较慢的选项,确保了在发生系统崩溃后数据的一致性。
* DSYNC; 也称为DATA_SYNC):要求仅文件内容的更新同步写入到存储设备,而元数据更新可以有不同的同步策略。
* 相比SYNC,这个选项提供了较好的性能折衷,但仍保证了数据的基本完整性。
*
* */
Files.writeString(path, ss);
}
}
3、新增 ZGC
Z Garbage Collector,这款垃圾回收器的主要特点是可伸缩的、低延迟。
-
低延迟:ZGC旨在实现暂停时间不超过10毫秒的目标。这对于要求极低停顿时间的应用(如金融服务、交易系统和大型Web服务)非常有利。
-
可扩展性:ZGC设计用于处理非常大的内存容量,能够高效管理数百GB乃至TB级别的堆内存,非常适合现代云基础设施中的大内存服务器。
-
并发执行:几乎所有垃圾回收工作都是并发执行的,包括标记、重定位(即移动存活对象)、压缩等过程,从而大大减少了应用程序的停顿时间。
-
无分代设计:ZGC摒弃了传统分代收集器中的年轻代和老年代的概念,转而采用统一的内存管理方式,虽然它内部有类似颜色的区分来优化垃圾回收效率,但对外呈现的是单一内存池。
-
可选择的内存限制:用户可以通过JVM参数设置堆内存的使用上限,ZGC会尝试保持堆的使用不超过这个限制,有助于容器化环境中的资源管理。
ZGC的使用:
ZGC在JDK11版本 ,只支持 Linux/x64 操作系统。
为了方便演示,我这里 使用JDK22版本在 Windows系统上演示。
ZGC垃圾回收器 需要手动指定。
在JVM的启动参数上新增如下参数:
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
-XX:+UnlockExperimentalVMOptions 允许使用实验性特性
-XX:+UseZGC 使用ZGC 作为垃圾回收器
从JDK 15开始,ZGC(Z Garbage Collector)就已经成为默认启用的垃圾收集器之一,无需再使用-XX:+UnlockExperimentalVMOptions来解锁实验性特性。这意味着从JDK 15及之后的版本可以直接通过 -XX:+UseZGC 选项来启用ZGC,而不需要额外的标志来解锁实验性功能。
添加如下JVM启动参数: -Xlog:gc*:file=gc.log:time
看下启动后的gc日志:
[2024-05-28T15:05:46.172+0800] Initializing The Z Garbage Collector // 初始化ZGC
[2024-05-28T15:05:46.172+0800] Version: 22.0.1+8-16 (release) // Java版本
[2024-05-28T15:05:46.172+0800] Using legacy single-generation mode // ZGC当前配置为使用传统的单代模式进行垃圾回收
[2024-05-28T15:05:46.172+0800] NUMA Support: Disabled // NUMA(非统一内存访问)优化未被启用
[2024-05-28T15:05:46.172+0800] CPUs: 8 total, 8 available // 系统总共有8个CPU核心,全部可用,并且可用内存为16.2GB
[2024-05-28T15:05:46.172+0800] Memory: 16207M // 系统总共有8个CPU核心,全部可用,并且可用内存为16.2GB
[2024-05-28T15:05:46.172+0800] Large Page Support: Disabled // 大页内存支持,大页可以减少内存管理开销
[2024-05-28T15:05:46.172+0800] GC Workers: 2 (dynamic) // 有2个动态调整的垃圾回收工作线程
[2024-05-28T15:05:46.172+0800] Address Space Type: Contiguous/Unrestricted/Complete // ZGC的地址空间配置,大小为9.6GB,采用连续、不受限、完整的地址空间类型
[2024-05-28T15:05:46.173+0800] Address Space Size: 3200M x 3 = 9600M // ZGC的地址空间配置,大小为9.6GB,采用连续、不受限、完整的地址空间类型
[2024-05-28T15:05:46.173+0800] Min Capacity: 8M // 最小堆容量8MB
[2024-05-28T15:05:46.173+0800] Initial Capacity: 200M //初始化堆容量200MB
[2024-05-28T15:05:46.173+0800] Max Capacity: 200M //最大堆容量200MB
[2024-05-28T15:05:46.173+0800] Medium Page Size: 4M // 设置为4MB,这通常用于内部内存分配和管理
[2024-05-28T15:05:46.173+0800] Pre-touch: Disabled // 不会预先分配和初始化所有内存页以减少以后的页面错误,这可能会在首次访问时增加延迟
[2024-05-28T15:05:46.173+0800] Uncommit: Enabled // 启用了解除提交内存功能,允许JVM释放不再需要的物理内存给操作系统,并设置了300秒的延迟
[2024-05-28T15:05:46.173+0800] Uncommit Delay: 300s
[2024-05-28T15:05:46.181+0800] Runtime Workers: 2 // 运行时工作线程: 同样为2个,与GC工作线程数一致
[2024-05-28T15:05:46.181+0800] Using The Z Garbage Collector // 开始使用 ZGC
关于ZGC更深入的知识点 在后面学习JVM的时候再写…
JDK17版本
1、增强版伪随机数生成器
在JDK17版本之前 我们要生成随机数据主要通过:
java.util.Random
java.security.SecureRandom
这两个实现都有局限性:
Random生成的随机数有可预测性:
java.util.Random 使用的是一个线性同余生成器(LCG)算法,Random 的输出是完全可预测的,如果我们知道种子和算法,完全可以生成相同的随机序列。这在需要高安全性的随机数生成(如加密)时是不合适的。
java.security.SecureRandom 是 Java 提供的一个用于生成加密强度随机数的类,专门为需要高安全性的应用场景设计,比如加密、安全令牌生成、会话密钥以及数字签名应用。
SecureRandom 优劣势分析:
优势 | 劣势 |
---|---|
安全性高: SecureRandom设计用于提供加密强度的随机数生成,这意味着它生成的随机数更难以预测,对于安全敏感的应用至关重要。它通常基于操作系统提供的强随机数生成器(例如 /dev/urandom 在Unix-like系统中),或者使用硬件随机数生成器(如果可用),以确保高度的不可预测性。 | 性能问题: 由于SecureRandom强调安全性,它的生成速度可能比非安全的随机数生成器(如java.util.Random)慢。特别是在大量需要随机数的场景下,这种性能差异可能会成为瓶颈。 |
可配置性: 用户可以选择不同的随机数生成算法,Java提供了多种算法供选择,比如SHA1PRNG、NativePRNG等,可以根据应用场景的安全需求和性能要求来定制。 | 资源消耗: 特别是当依赖于硬件随机数生成器时,如果硬件生成随机数的速度较慢或资源紧张,可能会对系统整体性能产生影响。 |
JDK17版本引入了 增强版伪随机数生成器
RandomGenerator 接口:
获取RandomGenerator 实例:
- 默认实现 RandomGenerator.getDefault()
- 使用RandomGenerator.of(String name)获取特定的随机数生成器,name 为预定义的生成器名称。
- 使用RandomGeneratorFactory 工厂 获取RandomGenerator 实例
代码示例:
import java.util.random.RandomGenerator;
import java.util.random.RandomGeneratorFactory;
public class TestA {
public static void main(String[] args) {
// 获取默认的 随机数生成器
RandomGenerator randomGenerator1 = RandomGenerator.getDefault();
// 获取指定的 随机数生成器
RandomGenerator randomGenerator2 = RandomGenerator.of("Xoshiro256PlusPlus");
// 使用 随机数生成工厂RandomGeneratorFactory 获取默认的随机数生成器
RandomGeneratorFactory<RandomGenerator> aDefault = RandomGeneratorFactory.getDefault();
RandomGenerator randomGenerator3 = aDefault.create();
// 使用 随机数生成工厂RandomGeneratorFactory 获取指定的随机数生成器
RandomGeneratorFactory<RandomGenerator> factory = RandomGeneratorFactory.of("Xoshiro256PlusPlus");
RandomGenerator randomGenerator4 = factory.create();
// 打印工厂中所有可用的随机数生成器
RandomGeneratorFactory.all().forEach((item)->System.out.println(item.name()));
}
}
RandomGeneratorFactory工厂中所有可用的随机数生成器如下:
随机数生成器名称 | 版本 | 使用算法 | 特性 |
---|---|---|---|
Random | JDK1.0 | 线性同余生成器 (LCG) | 简单、快速,但周期短且质量较低 |
SecureRandom | JDK1.1 | 依赖于底层平台提供的安全随机数生成算法 | 高质量、安全的随机数,适用于加密等安全需求,但是性能一般 |
L32X64MixRandom | JDK17 | LXM系列算法,基于线性同余和XorShift | 高性能、适合并行计算,适用于普通用途 |
L128X128MixRandom | JDK17 | LXM系列算法,基于线性同余和XorShift | 增强版L32X64,具有更长的周期和更高的质量 |
L64X128MixRandom | JDK17 | LXM系列算法,基于线性同余和XorShift | 适用于需要中等周期和质量的应用 |
L128X1024MixRandom | JDK17 | LXM系列算法,基于线性同余和XorShift | 超长周期,高质量,适用于高需求应用 |
L64X128StarStarRandom | JDK17 | LXM系列算法,基于线性同余和XorShift | 类似L64X128,但具有不同的混合策略 |
Xoshiro256PlusPlus | JDK17 | Xoshiro256++ 算法 | 高速、长周期,适用于高性能计算 |
L64X256MixRandom | JDK17 | LXM系列算法,基于线性同余和XorShift | 提供更长的周期,适用于需要更高质量随机数的场景 |
Xoroshiro128PlusPlus | JDK17 | Xoroshiro128++ 算法 | 高速、适合资源有限的环境 |
L128X256MixRandom | JDK17 | LXM系列算法,基于线性同余和XorShift | 非常长的周期,适用于对随机数质量要求极高的场景 |
SplittableRandom | JDK17 | 基于64位乘法哈希算法 | 高性能、线程安全,适合并行计算 |
L64X1024MixRandom | JDK17 | LXM系列算法,基于线性同余和XorShift | 超长周期,适用于需要极高质量随机数的场景 |
比较常用的方法
- 生成随机数:例如 nextInt()、nextInt(0, 100)、nextLong()
- 生成随机数流:例如 ints()、longs(),这些方法返回的是一个 Stream,对于生成大量随机数比较有用。
代码示例:
import java.util.random.RandomGenerator;
public class TestA {
public static void main(String[] args) {
// 获取默认的 随机数生成器
RandomGenerator randomGenerator = RandomGenerator.getDefault();
// 范围是 Integer.MIN_VALUE(-2^31)到 Integer.MAX_VALUE(2^31 - 1)之间的所有整数。
int i = randomGenerator.nextInt();
System.out.println(i);
// 生成 [0,100) 之间的整数 包含0,不包含100
int i1 = randomGenerator.nextInt(100);
System.out.println(i1);
// 生成 [50,100) 之间的整数 包含50,不包含100
int i2 = randomGenerator.nextInt(50,100);
System.out.println(i2);
// 获取无限随机数流 并截取前10个 默认的int类型最大范围
randomGenerator.ints().limit(10).forEach(System.out::println);
// 获取包含10个随机数的流 默认的int类型最小范围
randomGenerator.ints(10).forEach(System.out::println);
// 生成一个无限的范围在[10, 20)的随机数流,并截取前10个,直接打印
randomGenerator.ints(10, 20).limit(10).forEach(System.out::println);
}
}
2、增强版Switch
-
防止的Fall-through新写法 (守卫模式)
case xxx -> 可以不用谢break; 也能防止穿透 -
模式匹配(类型模式、空模式、常量模式)
测试这个Demo 需要稍微新点版本的 IDEA 我用的IntelliJ IDEA 2023.1
public class TestA {
public static void main(String[] args) {
Object[] objects = {"Java", 123, "C#", "PHP", 6.66, "DeepJava"};
for (Object obj : objects) {
switch (obj) {
case Integer intR -> System.out.println("整数型:" + intR);
case Float floatR -> System.out.println("浮点型:" + floatR);
case Double doubleR -> System.out.println("双精度浮点数:" + doubleR);
case String str && str.length() > 5 -> System.out.println("长度超过5的字符串:" + str);
case String str -> System.out.println("短字符串:" + str);
case null -> System.out.println("为空值");
default -> System.out.println("其他类型:" + obj);
}
}
}
}
JDK21版本
1、虚拟线程
虚拟线程在 JDK 19 版本中作为预览特性引入,旨在简化和改善 Java 的并发模型。
在JDK 21版本 虚拟线程已经被作为正式特性。
理解平台线程和系统内核线程的对应关系:
在JVM中 java.lang.Thread 就代表平台线程, HotSpot 虚拟机在Windows和Linux操作系统下,一个平台线程对应一个内核线程(在其他操作系统下 这个对应关系就不一定是1:1了)。
如下图所示:
平台线程的优劣势
优势:
优势 | 描述 | 举例 |
---|---|---|
一对一映射 | 在JVM中,java.lang.Thread 代表平台线程。HotSpot虚拟机在Windows和Linux操作系统下,一个平台线程对应一个内核线程。 | 每个Java平台线程对应一个由操作系统管理的内核线程。 |
多线程并发 | 平台线程允许Java程序进行并发操作,提高程序的并发能力和资源利用率。 | 可以在一个线程中进行网络请求,在另一个线程中进行文件读写。 |
硬件加速 | 平台线程可以利用操作系统的多核处理能力,实现真正的并行计算。 | 高性能计算应用程序可以通过多核处理实现并行计算。 |
劣势:
劣势 | 描述 | 举例 |
---|---|---|
高开销 | 平台线程的创建和销毁成本较高,涉及操作系统级别的资源分配和管理。 | 服务器应用为每个请求创建一个新线程,消耗大量资源。 |
线程数量受限 | 操作系统能够支持的线程数量有限,在高并发场景下可能很快达到资源限制。 | 高并发Web服务器可能因线程资源耗尽而无法响应新请求。 |
阻塞问题 | 使用阻塞I/O操作时,平台线程会一直占用内核线程,导致资源浪费。 | 文件读写操作使用阻塞I/O时,线程在操作完成前一直阻塞。 |
上下文切换开销 | 平台线程的上下文切换开销较大,涉及寄存器、内存等资源的切换。 | 多线程应用频繁进行线程上下文切换,降低程序执行效率。 |
理解虚拟线程和平台线程的对应关系:
上面说了在JVM中 java.lang.Thread 就代表平台线程, 在JDK19开始引入虚拟线程后,为了使得虚拟线程的使用能够如同使用平台线程一样,所以 java.lang.Thread 也代表了虚拟线程,只是二者的创建方式和底层实现不同,对平台线程和虚拟线程的抽象都是用 java.lang.Thread 表示。
这么设计最直接的好处就是虚拟线程的API与平台线程相同,开发者无需学习新的并发模型或API,可以直接在现有代码中使用虚拟线程。这使得现有的多线程代码可以无缝地转换为使用虚拟线程。
如下图所示:
虚拟线程的优劣势
优势:
优势 | 描述 | 平台线程对比 |
---|---|---|
轻量级 | 虚拟线程消耗的系统资源更少,可以在同一个JVM进程中创建和管理大量线程。(甚至可以创建上万数量级的虚拟线程) | 平台线程创建和销毁成本高,涉及操作系统级别的资源管理。 |
高并发 | 能够同时运行数万个虚拟线程,甚至更高,提高并发能力,特别适用于I/O密集型任务。 | 平台线程数量受限,高并发场景下可能达到系统资源限制。 但是平台线程更适合做密集计算和精细控制线程行为的场景 |
简单的同步编程 | 支持传统的同步阻塞编程模型,开发者无需学习新的并发模型,代码更易理解和维护。 | |
高效利用CPU | 虚拟线程在等待I/O操作时可以释放平台载体线程去执行其他任务,提高CPU利用率。 | 平台线程上下文切换开销较大,频繁切换会影响系统性能。 |
工具链兼容 | 调试器、分析工具和其他基于Thread API的工具无需修改即可支持虚拟线程。 | 平台线程已经广泛支持现有工具链,但管理和调试大量线程复杂且资源消耗大。 |
劣势:
劣势 | 描述 | 平台线程对比 |
---|---|---|
性能不确定性 | 在某些极端情况下,虚拟线程的性能可能无法预期,需要实际测试和调整。 | 平台线程性能稳定,但由于高开销和数量限制,扩展性差。 |
总结下平台线程和虚拟线程的适用场景
平台线程的适用场景
场景 | 描述 | 示例 |
---|---|---|
高性能计算 | 平台线程能够充分利用多核处理器的并行计算能力,适用于CPU密集型任务。 | 科学计算、图像处理、视频编码等需要大量计算资源的应用。 |
需要操作系统级特性的应用 | 平台线程可以直接利用操作系统提供的线程管理和调度功能,适用于需要与操作系统深度交互的任务。 | 低延迟、高可靠性的系统服务,如数据库管理系统、网络服务器等。 |
固定数量的并发任务 | 适用于并发任务数量相对固定且有限的场景,线程管理和资源开销可以接受。 | 传统的Web服务器、应用服务器等,处理相对少量并发请求。 |
阻塞I/O操作 | 虽然平台线程在阻塞I/O操作时会占用内核线程,但在某些情况下,平台线程的使用仍然合适。 | 需要处理少量阻塞I/O操作且对线程资源消耗不敏感的应用,如简单的文件读写操作。 |
虚拟线程的适用场景
场景 | 描述 | 示例 |
---|---|---|
高并发、I/O密集型任务 | 虚拟线程可以轻松管理大量并发任务,特别适用于I/O密集型操作,如网络通信、文件读写等。 | 高并发的Web服务器、微服务架构中的各个服务节点。 |
轻量级并发任务 | 虚拟线程创建和销毁成本低,非常适合需要频繁创建和销毁线程的应用。 | 实时聊天应用、大规模的消息处理系统。 |
复杂的业务逻辑 | 虚拟线程支持简单的同步阻塞编程模型,使得编写和维护复杂的业务逻辑变得更加容易。 | 涉及复杂数据处理和业务流程的后台服务。 |
高扩展性要求 | 虚拟线程能够在不显著增加系统资源开销的情况下支持数万个并发任务,适用于需要高扩展性的应用。 | 物联网(IoT)平台、大规模在线游戏服务器。 |
资源受限环境 | 虚拟线程在资源受限的环境下表现出色,可以显著减少内存和CPU的使用。 | 嵌入式系统、边缘计算设备。 |
运行下面的代码 建议IDEA使用新点的版本,我使用的是IntelliJ IDEA 2023.3.6。
虚拟线程的创建代码示例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
public class TestA {
public static void main(String[] args){
// 方式一、Thread.ofVirtual()
Thread thread = Thread.ofVirtual().name("虚拟线程").start(()->{
System.out.println("虚拟线程1执行任务");
});
// 方式二、Thread.startVirtualThread()
Thread.startVirtualThread(()-> System.out.println("虚拟线程执行任务"));
// 方式三、线程池
try (ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();){
executorService.execute(()->{
System.out.println("虚拟线程执行任务");
});
} catch (Exception e) {
throw new RuntimeException(e);
}
//方式四、通过 ThreadFactory 创建
// 获取线程工厂类
ThreadFactory factory = Thread.ofVirtual().factory();
Thread newThread = factory.newThread(() -> {
System.out.println("虚拟线程执行任务");
});
newThread.start();
}
}
2、序列化集合
主要有以下三个接口:
SequencedCollection
SequencedSet
SequencedMap
目的是提供统一的方法来获取集合的第一个元素和最后一个元素。
下面用 ArrayList,LinkedHashSet,LinkedHashMap举例
import java.util.*;
public class TestA {
public static void main(String[] args) {
List<String> list = new ArrayList<>(Arrays.asList("1", "2", "3"));
System.out.println(list.getFirst()); // 1
System.out.println(list.getLast()); // 3
LinkedHashSet<String> hashSet = new LinkedHashSet<>();
hashSet.add("1");
hashSet.add("2");
hashSet.add("3");
System.out.println(hashSet.getFirst()); // 1
System.out.println(hashSet.getLast()); // 3
LinkedHashMap<String, String> map = new LinkedHashMap<>();
map.put("1","1");
map.put("2","2");
map.put("3","3");
System.out.println(map.firstEntry()); // 1=1
System.out.println(map.lastEntry()); // 3=3
}
}