Java 8 是 Java 语言的一次重大升级,引入了许多新的特性和方法,包括以下几个方面:
- Lambda 表达式:Lambda 表达式是 Java 8 中最重要的新增特性,它允许我们更便捷地创建简单的功能接口,进而实现函数式编程。
函数式编程(Functional Programming)是一种编程范式,它强调函数的使用和运算过程中避免改变状态和可变数据。在函数式编程中,函数被视为一等公民,它们可以像其他值一样被传递、赋值和返回。因此函数式编程通常可以处理复杂的问题,而且代码可读性和可维护性也较高。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 使用 Lambda 表达式实现一个简单的过滤器来选择大于 3 的数字
List<Integer> filteredNumbers = numbers.stream()
.filter(n -> n > 3)
.collect(Collectors.toList());
System.out.println(filteredNumbers); // 输出 [4, 5]
- Stream API:Stream API 提供了一种易于使用和并行化的方法来处理集合数据。我们可以使用 Stream API 来过滤、映射、聚合等操作集合中的数据。
Stream API 提供了众多操作集合的方法,这里列出一些常用的方法及其作用:
2.1. filter(Predicate<T> predicate)
:过滤掉不符合条件的元素。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> filteredNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(filteredNumbers); // [2, 4]
2.2. map(Function<T, R> mapper)
:对元素进行转换。
List<String> words = Arrays.asList("Java", "is", "fun");
List<Integer> wordLengths = words.stream()
.map(String::length)
.collect(Collectors.toList());
System.out.println(wordLengths); // [4, 2, 3]
2.3. forEach(Consumer<T> action)
:遍历集合中的每个元素并执行指定的操作。
List<String> words = Arrays.asList("Java", "is", "fun");
words.stream().forEach(System.out::println); // Java is fun
2.4. reduce(T identity, BinaryOperator<T> accumulator)
:通过二元操作将所有元素组合为一个值。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().reduce(0, Integer::sum);
System.out.println(sum); // 15
2.5. sorted(Comparator<T> comparator)
:对元素进行排序。
List<Integer> numbers = Arrays.asList(3,1,4,5,2);
List<Integer> sortedNumbers = numbers.stream()
.sorted()
.collect(Collectors.toList());
System.out.println(sortedNumbers); // [1, 2, 3, 4, 5]
2.6. distinct()
:过滤掉重复的元素。
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 4, 5);
List<Integer> distinctNumbers = numbers.stream().distinct().collect(Collectors.toList());
System.out.println(distinctNumbers); // [1, 2, 3, 4, 5]
3. 方法引用:方法引用允许我们使用已有的方法来代替 Lambda 表达式,从而更加简化代码。
方法引用(Method Reference)是 Java 8 中的一个新特性,它允许我们使用已经存在的方法(静态方法、实例方法、构造函数)来代替 Lambda 表达式。这样可以进一步简化代码,提高可读性。
方法引用有以下几种形式:
3.1. 静态方法引用:Class::staticMethod
List<String> words = Arrays.asList("Java", "is", "fun");
words.stream().map(String::toUpperCase).forEach(System.out::println); // JAVA IS FUN
在这个例子中,String::toUpperCase
就是一个静态方法引用,它等价于使用 Lambda 表达式:s -> s.toUpperCase()
。
3.2. 实例方法引用:instance::method
List<String> words = Arrays.asList("Java", "is", "fun");
words.stream().map(String::length).forEach(System.out::println); // 4 2 3
在这个例子中,String::length
是一个实例方法引用,它等价于使用 Lambda 表达式:s -> s.length()
。
3.3. 构造函数引用:ClassName::new
List<String> words = Arrays.asList("Java", "is", "fun");
List<String> upperCaseWords = words.stream().map(String::new).map(String::toUpperCase).collect(Collectors.toList());
System.out.println(upperCaseWords); // [JAVA, IS, FUN]
在这个例子中,String::new
是一个构造函数引用,它等价于使用 Lambda 表达式:(s) -> new String(s)
。
方法引用可以让代码更加简洁易读,但需要注意的是,方法引用适用于简单的函数式接口,而对于复杂的接口,可能还需要使用 Lambda 表达式来实现。
- 接口中的默认方法:接口中可以有默认方法,这些方法可以在不破坏现有 API 的情况下添加新功能。
interface MyInterface {
default void sayHello() {
System.out.println("Hello,我是 MyInterface 中的默认方法");
}
}
class MyClass implements MyInterface {
// 这里可以不实现 sayHello 方法,因为默认方法已经提供了实现
}
public class Main {
public static void main(String[] args) {
MyInterface myObject = new MyClass();
myObject.sayHello();
// 输出 Hello,我是 MyInterface 中的默认方法 } }
Java 8 中引入了接口中的默认方法(Default Method),它可以在接口中添加新的方法,而不会影响到现有的实现类。
默认方法是指接口中可以有方法体的方法,可以为接口提供默认实现。接口中的默认方法在实现类中可以通过简单的方法调用来使用,而不需要去实现这些方法。
以下是一个默认方法的示例:
interface MyInterface {
default void sayHello() {
System.out.println("Hello,我是 MyInterface 中的默认方法");
}
}
class MyClass implements MyInterface {
// 这里可以不实现 sayHello 方法,因为默认方法已经提供了实现
}
public class Main {
public static void main(String[] args) {
MyInterface myObject = new MyClass();
myObject.sayHello(); // 输出 Hello,我是 MyInterface 中的默认方法
}
}
在这个例子中,接口 `MyInterface` 中定义了一个默认方法 `sayHello()`,该方法提供了一个输出语句的默认实现。类 `MyClass` 实现了接口 `MyInterface`,但它并没有提供 `sayHello()` 方法的具体实现。在 `Main` 方法中,我们创建了一个 `MyClass` 的实例,并调用了 `sayHello()` 方法。由于接口中提供了默认方法的实现,因此程序将输出默认的 hello 语句。
接口中的默认方法为我们提供了一种方便的扩展方式,可以为接口添加新的功能而不会影响到现有的实现类。在实际编程中,需要适度运用默认方法,以便充分利用这个特性。
- 可重复注解:Java 8 允许我们在同一个地方使用同一个注解多次,从而更加灵活地使用注解。
Java 8 引入了可重复注解(Repeated Annotation)的概念,它允许在同一个地方使用同一个注解多次,从而更加灵活地使用注解。使用可重复注解,我们可以在一个元素上多次注解同一个注解类型,或者在同一个元素上注解多个不同的注解类型。
下面是一个使用可重复注解的例子:
@Repeatable(Fruits.class)
@interface Fruit {
String value();
}
@Retention(RetentionPolicy.RUNTIME)
@interface Fruits {
Fruit[] value();
}
@Fruit("apple")
@Fruit("peach")
public class Main {
public static void main(String[] args) {
Fruit[] fruitArray = Main.class.getAnnotationsByType(Fruit.class);
for (Fruit fruit : fruitArray) {
System.out.println(fruit.value());
}
}
}
**
在这个例子中,我们定义了一个注解类型 @Fruit
,它本身是一个容器注解,用于存储多个 @Fruit
注解。而 @Fruit
注解本身也是一个 ElementType.ANNOTATION_TYPE 类型的注解,用于标注水果名称。在 Main
类上,我们使用了可重复注解,将两个水果名称 “apple” 和 “peach” 注解到了 Main
类上。在 main()
方法中,我们使用了 getAnnotationsByType()
方法来获取 Main
类上的所有 @Fruit
注解,并打印出注解中的水果名称。
通过使用可重复注解,我们可以更加灵活地使用注解,避免了在代码中出现大量的注解嵌套。不过需要注意的是,在使用可重复注解时,需要为注解类型定义一个容器类型,并指定它的 @Repeatable
注解。
- 类型注解:Java 8 引入了一种新的注解类型 ElementType.TYPE_PARAMETER,允许我们对类型使用注解。
类型注解(Type Annotation)的概念,它允许我们对类型、类型参数、类型转换、方法返回类型等使用注解。在之前的 Java 版本中,注解只能应用于声明和类型使用等位置上,而无法直接应用于类型本身。类型注解的引入,增强了 Java 的类型检查机制,并提供了更加灵活的注解使用方式。
以下是一个类型注解的示例:
@Target(ElementType.TYPE_PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation {}
class MyClass<@MyAnnotation T> {
public <@MyAnnotation U> void myMethod(@MyAnnotation T t, @MyAnnotation U u) {}
}
public class Main {
public static void main(String[] args) throws NoSuchFieldException {
TypeVariable<TypeParameter<Main, MyAnnotation>>[] typeParameters = MyClass.class.getTypeParameters();
for (TypeVariable<TypeParameter<Main, MyAnnotation>> typeParameter : typeParameters) {
Annotation[] annotations = typeParameter.getAnnotations();
for (Annotation annotation : annotations) {
System.out.println(annotation);
}
}
}
}
**
在这个例子中,我们定义了一个注解类型 MyAnnotation
,并将它应用于了泛型类型参数 T
,以及方法参数 t
和 u
上。在 main()
方法中,我们使用反射的方式获取 MyClass
上的类型参数,并打印出类型参数上的注解信息。
通过引入类型注解,我们可以为类型的使用场景提供更丰富的语义,以及更加细粒度的约束条件。
-
新的日期/时间 API:Java 8 引入了新的日期和时间 API,支持日期和时间操作,包括计算、格式化、解析、比较等操作。
并发相关的特性,这些特性包括但不限于以下几个方面:
7.1. CompletableFuture 类:该类提供了基于回调机制的异步编程模型,支持链式调用和组合多个异步操作。
以下是一个使用 CompletableFuture 类的示例:
import java.util.concurrent.CompletableFuture;
public class Main {
public static void main(String[] args) {
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> future = completableFuture.thenApplyAsync(s -> s + " World");
future.thenAcceptAsync(System.out::println);
}
}
**
在这个例子中,我们创建了一个 CompletableFuture 对象 completableFuture
,并在其上调用了 thenApplyAsync()
方法,该方法会在计算完成后对结果进行转换,生成一个新的 CompletableFuture 对象 future
。最后,我们使用 thenAcceptAsync()
方法消费 future
的结果并打印出来。
7.2. 新的并行数组:Java 8 引入了新的数组类 IntStream、LongStream、DoubleStream,它们提供了并行计算数组元素的方法,从而提高了数组的处理效率。
int[] array = new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
//顺序计算平均值
double avg1 = Arrays.stream(array).average().orElse(Double.NaN);
System.out.println("顺序计算平均值:" + avg1);
//并行计算平均值
double avg2 = Arrays.stream(array).parallel().average().orElse(Double.NaN);
System.out.println("并行计算平均值:" + avg2);
//更加复杂的,并行计算示例
int[] newArr = IntStream.range(0, 1000_000).parallel().map(x -> x * 2).filter(x -> x % 3 == 0).toArray();
System.out.println(Arrays.toString(Arrays.copyOf(newArr, 10)));
7.3. Stream 接口的并行化:在 Java 8 中,Stream 接口提供了并行化处理数据的方法,它允许数据分割成多个小块,为每个小块创建一个线程来处理,最终将结果合并起来。
Stream 接口中,有两个方法可以启用并行处理方式:`parallel()` 和 `parallelStream()`。`parallel()` 方法可以让一个现存的普通的流并行化,`parallelStream()` 方法可以直接创建并行化的流。并行化后的流会将数据分割成多个小块,并为每个小块创建一个线程来处理,最终将结果合并起来。
下面是一个使用 Stream 并行化处理数据的示例:
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
String[] strings = {"a", "b", "c", "d", "e"};
long count1 = Stream.of(strings).filter(s -> s.contains("a")).count();
System.out.println(count1 + " 个包含字母 a 的字符串(顺序流)");
long count2 = Stream.of(strings).parallel().filter(s -> s.contains("a")).count();
System.out.println(count2 + " 个包含字母 a 的字符串(并行流)");
}
}
**
在这个示例中,我们使用 Stream.of() 方法创建一个普通的流,然后使用 filter()
方法过滤包含字母 a 的字符串,并使用 count()
方法计算数量。首先,我们使用顺序流进行计算,然后使用 parallel()
方法将流转换为并行流进行计算,并比较两种方式的结果。
使用 Stream 接口进行并行化处理数据的方式提高了数据处理的效率,特别是当数据量很大时。然而,在实践中,需要注意以下几个方面:
7.3.1.
Java 8 建议使用默认的 Fork/Join 并行框架(默认的 ForkJoinPool)来进行并行处理,这个框架可以在多核处理器上更好地利用线程和执行器,并提供了针对各种情况的优化策略。
7.3.2.
在实践中,需要仔细评估并行化的处理流程是否能够真正地提高数据处理效率,是否会带来不必要的并行化损耗。如果处理流程本身是短暂的或者数据集小,使用并行化处理可能并不利于提高效率。
7.3.3.
在启用 Stream 的并行化处理时,需要避免使用状态变量,避免数据的竞争和不可确定性。避免使用副作用,尽量保证操作的无状态性。
7.4. StampedLock 类:该类提供了一种乐观锁的机制,适用于读多写少的场景,能够提高锁的并发性能。
StampedLock 类的使用方式类似于 ReentrantReadWriteLock 类,但它比后者更加灵活和高效。相对于传统的读写锁,StampedLock 的乐观读锁机制可以显著提高读取数据的性能,在读多写少的场景下,StampedLock 的性能比 ReentrantReadWriteLock 更好。
下面是一个使用 StampedLock 类的示例:
import java.util.concurrent.locks.StampedLock;
public class Main {
private double x, y;
private final StampedLock lock = new StampedLock();
public void move(double deltaX, double deltaY) {
long stamp = lock.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
lock.unlockWrite(stamp);
}
}
public double distanceFromOrigin() {
long stamp = lock.tryOptimisticRead();
double currentX = x, currentY = y;
if (!lock.validate(stamp)) {
stamp = lock.readLock();
try {
currentX = x;
currentY = y;
} finally {
lock.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
**
在这个示例中,我们使用 StampedLock 类来实现一个二维平面上的点,并提供了两种方法来移动点和计算距离。在 move()
方法中,我们使用 writeLock()
方法获取写锁,并对点进行移动。在 distanceFromOrigin()
方法中,我们使用 tryOptimisticRead()
方法尝试获取一个乐观读锁,然后再判断锁是否可用,如果不可用,则使用 readLock()
方法获取一个读锁,计算点到原点的距离,并返回结果。
不过需要注意的是,StampedLock 类并不适用于大量写、少量读的场景,
7.5. 并行计算框架 Fork/Join:Java 8 中提供了 Fork/Join 框架,能够方便地编写并行化的递归算法。
Java 8 中的并发新特性能够以更加简洁、高效和安全的方式处理并发编程,极大地提高了编程效率和应用性能。在实际编程中,需要根据实际场景选择合适的并发处理技术 8. 并发新特性:Java 8 中增加了许多新的并发相关的特性,如 CompletableFuture、StampedLock、并行数组等。
-
Optional 类:Optional 类是 Java 8 中新增的一个类,用于避免程序中出现 null 指针异常。可以避免很多经典问题,比如怎样避免在返回值里传递 null、怎样编写容错与非容错代码、怎样处理嵌套的 null-checks。
Optional 类是 Java 8 中非常有用的一个类,用于避免程序中出现 null 指针异常。在之前的 Java 版本中,我们通常需要在程序中进行繁琐的 null 值检查,过多的 null 检测也会使代码显得冗长和不易维护。
Optional 类可以用来包装任意类型的值,并提供了一组操作方法来处理这个值,避免了 null 检测的问题。如果原始值为非 null,那么调用 Optional 类实例上的方法将会返回原始值,否则返回一个默认值或者抛出一个异常,这样可以有效防止 null 值的传递和对 null 值的操作。
下面是一个使用 Optional 类的示例:
import java.util.Optional;
public class Main { public static void main(String[] args) { String str1 = null; String str2 = "hello, world!";
Optional<String> optional1 = Optional.ofNullable(str1);
Optional<String> optional2 = Optional.ofNullable(str2);
System.out.println(optional1.isPresent()); // false
System.out.println(optional2.isPresent()); // true
optional1.ifPresent(System.out::println); // does not print
optional2.ifPresent(System.out::println); // "hello, world!"
String s1 = optional1.orElse("fallback"); // "fallback"
String s2 = optional2.orElse("fallback"); // "hello, world!"
String s3 = optional1.orElseGet(() -> "computed"); // "computed"
String s4 = optional2.orElseGet(() -> "computed"); // "hello, world!"
optional1.orElseThrow(IllegalArgumentException::new); // throws IllegalArgumentException
optional2.orElseThrow(IllegalArgumentException::new); // returns "hello, world!"
}
} 在这个示例中,我们使用 Optional 类对两个字符串进行包装,并进行了多种操作。我们使用 ofNullable() 方法将字符串包装到 Optional 对象中。然后使用 isPresent() 方法检查对象是否包含非空值。接下来,我们使用 ifPresent() 方法,根据是否存在非空值执行相关操作。我们还使用了 orElse()、orElseGet() 和 orElseThrow() 方法来获取值或提供默认值。
使用 Optional 类可以让代码更加简洁、健壮和容错,防止因 null 值产生的异常和错误。尤其在函数式编程和流式计算中,Optional 类的使用更加广泛,提高了代码的可读性和安全性。
ppppppps `
ObjectUtil.isNotEmpty()
方法跟 Optional
类的使用方式有一些不同。
ObjectUtil.isNotEmpty()
方法是通过判断传入的对象是否为空或者是否是空字符串来判断对象是否为空,返回布尔值。这个方法主要适用于简单的判空场景,例如检查一个字符串对象是否为空。
而 Optional
类的主要作用是用来包装可能为空的对象,通过一系列的方法来处理这个对象。它提供了一组操作方法来处理包装的对象,例如检查对象是否包含值、获取或设置对象的值、以及在对象为空时提供默认值等。Optional
类适用于需要对返回值进行各种操作(例如获取对象的某个属性)的场景。
所以,如果只是对简单对象进行判空,可以使用 ObjectUtil.isNotEmpty()
方法;如果需要对对象进行一系列操作,建议使用 Optional
类来处理可能为空的对象,以提高代码的健壮性和可读性。
`
除了上述特性,Java 8 还增加了很多其他改进,例如 Base64、Nashorn JavaScript 引擎、ConcurrentHashMap 等。这些新的特性使得 Java 8 更加强大和灵活,在不改变现有代码结构的情况下,让 Java 程序变得更加简单、优雅、高效。