文章目录
Java 8 进阶知识和用法
还不了解java8 基础知识的可以先看这两篇文章:
《Java 8 新特性:Lambda 表达式与 Stream 流,重构你的编码效率(上篇:Lambda 表达式)》
《Java 8 新特性:Lambda 表达式与 Stream 流,重构你的编码效率(下篇:stream流)》
1. Lambda 表达式的深入理解
1.1 捕获变量
Lambda 表达式可以访问其外围作用域中的变量,这些变量被称为捕获变量。为了保证线程安全和避免副作用,Java 对捕获变量有一些限制:
- 有效最终性:捕获变量必须是“实际上不可变”的,也就是说,它们要么是
final
的,要么是在定义后没有被修改的局部变量。 - 不可变性:一旦一个变量被 lambda 表达式引用,它就不能再被修改。如果尝试修改这样的变量,编译器会报错。
- 不可变容器:如果 lambda 需要访问容器中的元素,那么这个容器也应该是不可变的,或者使用不可变视图。
示例:
int x = 10;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.forEach(n -> System.out.println(n + x)); // x 是捕获变量
1.2 尾递归优化
Java 本身并不支持尾递归优化。尾递归是指递归函数的最后一步调用自身的情况。由于 Java 虚拟机(JVM)不进行尾调用优化,因此递归调用可能会导致栈溢出错误。
替代方案:
- 迭代:将递归转换为迭代通常是一个可行的选择。
- 显式堆栈:使用显式的数据结构(如堆栈)来模拟递归行为。
- 尾递归优化库:有些第三方库提供了尾递归优化的支持。
示例:
public static int factorial(int n) {
return factorialHelper(n, 1);
}
private static int factorialHelper(int n, int accumulator) {
if (n <= 1) return accumulator;
return factorialHelper(n - 1, n * accumulator);
}
1.3 性能考量
Lambda 表达式的性能取决于多个因素:
- 对象创建成本:每个 lambda 表达式都会生成一个匿名类实例,这会导致额外的对象创建开销。
- 方法句柄调用:lambda 表达式的执行涉及到方法句柄的调用,这可能比直接调用方法稍慢。
- 内联优化:JIT 编译器可能会对简单的 lambda 表达式进行内联优化,从而提高性能。
- 并行处理:当使用并行流时,lambda 表达式的开销可能被多线程处理带来的性能提升所抵消。
最佳实践:
- 简单表达式:对于简单的操作,使用 lambda 可以提高代码可读性和简洁性,而性能损失通常可以忽略。
- 避免复杂计算:对于复杂的计算或大量的数据处理,考虑使用传统的循环或显式方法调用来减少对象创建次数。
- 并行流:对于大数据集,使用并行流可以显著提高处理速度,即使存在额外的 lambda 创建开销。
示例:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
long sum = numbers.stream()
.filter(n -> n % 2 == 0)
.mapToInt(Integer::intValue)
.sum();
2. 函数式接口的深入理解
2.1 @FunctionalInterface 注解
在 Java 中,函数式接口是指只有一个抽象方法的接口。这种接口特别适合用于 lambda 表达式,因为 lambda 表达式本身就是用来实现单个方法的行为的。为了明确一个接口是函数式接口,可以使用 @FunctionalInterface
注解。
示例:
@FunctionalInterface
public interface MyFunction<T, R> {
R apply(T t);
// 可以有默认方法或静态方法
default void defaultMethod() {
System.out.println("This is a default method.");
}
static void staticMethod() {
System.out.println("This is a static method.");
}
}
在这个例子中,MyFunction
接口有一个抽象方法 apply
,并且使用了 @FunctionalInterface
注解。注解的存在有助于确保接口确实是函数式的,并且只包含一个抽象方法;如果试图添加第二个抽象方法,编译器将会抛出错误。
2.2 函数组合
Java 8 引入了许多内置的函数式接口,如 Function
, Predicate
, Consumer
, 和 Supplier
。这些接口提供了默认的方法来支持函数组合,允许开发者通过简单的操作来构建更复杂的逻辑。
示例:
假设我们有两个函数式接口,Function
和 Predicate
,我们可以将它们组合起来创建新的逻辑。
- 组合两个 Function:
使用andThen
或compose
方法来组合两个Function
实例。andThen
方法先应用第一个函数,然后将结果传递给第二个函数;而compose
方法则相反,先应用第二个函数,然后将结果传递给第一个函数。
示例:
Function<Integer, Integer> addOne = x -> x + 1;
Function<Integer, Integer> multiplyTwo = x -> x * 2;
// 先加一,然后乘二
Function<Integer, Integer> addOneAndMultiplyTwo = addOne.andThen(multiplyTwo);
int result = addOneAndMultiplyTwo.apply(5); // 结果为 12
- 组合 Function 和 Predicate:
使用Function
和Predicate
的组合来过滤和转换数据。
示例:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
Predicate<String> startsWithA = s -> s.startsWith("A");
Function<String, String> toUpperCase = String::toUpperCase;
List<String> filteredNames = names.stream()
.filter(startsWithA)
.map(toUpperCase)
.collect(Collectors.toList());
System.out.println(filteredNames); // 输出 ["ALICE"]
- 组合 Predicate:
使用and
,or
, 和negate
方法来组合Predicate
实例。这些方法分别代表逻辑与、逻辑或和逻辑非操作。
示例:
Predicate<Integer> isEven = x -> x % 2 == 0;
Predicate<Integer> isPositive = x -> x > 0;
Predicate<Integer> isEvenAndPositive = isEven.and(isPositive);
boolean result = isEvenAndPositive.test(4); // 结果为 true
通过上述示例,可以看出函数组合是一种非常强大的工具,可以用来简化代码和提高代码的可读性。此外,由于这些组合方法返回新的函数式接口实例,因此它们支持链式调用,使得代码更加流畅和易于理解。
3. 方法引用的深入理解
3.1 不同类型的引用
方法引用是一种更简洁的 lambda 表达式形式,它允许你直接引用现有方法而不是编写整个 lambda 表达式。方法引用可以分为几种类型:
- 静态方法引用:如果你想要引用一个静态方法,可以直接使用类名加上方法名。适用于接受一个或多个参数并返回一个值的静态方法。
示例:
public class MathUtils {
public static int add(int a, int b) {
return a + b;
}
}
Function<Integer, Integer> increment = MathUtils::add;
int result = increment.apply(1); // 需要传入第二个参数
- 实例方法引用:当你想要引用一个对象实例上的方法时,你可以使用对象实例引用。这适用于已经有一个实例对象的情况。
示示例:
public class Person {
public String getName() {
return "John Doe";
}
}
Person john = new Person();
Supplier<String> nameSupplier = john::getName;
String name = nameSupplier.get(); // 输出 "John Doe"
- 特定类型的实例方法引用:当你想要引用一个特定类型的实例方法时,你可以使用类名加上方法名的形式。这适用于当你还没有具体的实例对象时。
示例:
Function<String, Integer> stringToInt = Integer::parseInt;
int number = stringToInt.apply("123"); // 结果为 123
- 构造器引用:当你想要引用一个类的构造器时,你可以使用类名加上
::new
的形式。这适用于创建对象实例的场景。
示例:
Supplier<Person> personSupplier = Person::new;
Person jane = personSupplier.get();
- 数组构造器引用:当你想要引用一个数组的构造器时,可以使用数组类型加上
::new
的形式。
示例:
Function<Integer, Integer[]> arraySupplier = Integer[]::new;
Integer[] integers = arraySupplier.apply(5); // 创建长度为5的Integer数组
3.2 选择合适的引用类型
选择合适的方法引用类型主要取决于你的具体需求和上下文。以下是一些指导原则:
- 使用静态方法引用:当你需要引用一个不依赖于任何实例状态的静态方法时。
- 使用实例方法引用:当你已经有了一个具体的对象实例,并且想要引用该实例上的方法时。
- 使用特定类型的实例方法引用:当你知道目标对象的类型,但尚未创建具体实例时。
- 使用构造器引用:当你需要创建一个新的对象实例,并且构造器符合函数式接口的要求时。
- 使用数组构造器引用:当你需要创建数组实例时。
示例:
假设我们有一个 Person
类,我们想要创建一个新的 Person
实例,并使用 Person
的构造器来设置名字。
class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
// 创建 Person 实例
Function<String, Person> createPerson = Person::new;
Person alice = createPerson.apply("Alice");
System.out.println(alice.getName()); // 输出 "Alice"
在这个例子中,我们使用了构造器引用 Person::new
来创建一个新的 Person
实例。选择合适的方法引用类型可以使代码更加简洁和易读,同时减少了不必要的代码重复。
4. Stream API 的深入理解
4.1 中间操作与终结操作
Stream API 提供了一系列的操作来处理集合数据,这些操作可以分为两类:中间操作和终结操作。
-
中间操作:这些操作被设计成可以链接在一起,形成一个流水线。中间操作不会执行任何计算,而是返回一个新的流。例如
filter
,map
,sorted
等。 -
终结操作:这些操作会产生一个结果或者副作用,例如
forEach
,reduce
,collect
等。一旦执行了终结操作,流就被消费完毕,不能再在其上执行其他操作。
示例:
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Diana");
// 中间操作: filter 和 map
// 终结操作: forEach
names.stream()
.filter(name -> name.length() > 4)
.map(String::toUpperCase)
.forEach(System.out::println);
4.2 并行流
并行流通过将数据分成多个部分并并行处理这些部分来加速处理过程。这对于处理大量数据尤其有用,因为可以充分利用多核处理器的优势。
示例:
List<Integer> numbers = IntStream.rangeClosed(1, 10000000).boxed().collect(Collectors.toList());
// 使用并行流计算总和
long sum = numbers.parallelStream()
.filter(n -> n % 2 == 0)
.mapToLong(Integer::longValue)
.sum();
System.out.println("Sum of even numbers: " + sum);
4.3 短路操作
短路操作是指那些一旦找到满足条件的结果就会立即终止流的操作。这些操作可以提高效率,尤其是在处理大型数据集时。
findFirst()
:返回第一个元素(如果存在),一旦找到就立即停止。anyMatch()
:如果至少有一个元素满足条件,则返回true
,一旦找到匹配项就立即停止。noneMatch()
:如果没有任何元素满足条件,则返回true
,一旦找到不匹配的项就立即停止。allMatch()
:如果所有元素都满足条件,则返回true
,一旦发现不满足条件的元素就立即停止。
示例:
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 查找第一个大于3的元素
Optional<Integer> firstGreaterThanThree = numbers.stream()
.filter(n -> n > 3)
.findFirst();
System.out.println(firstGreaterThanThree.orElse(null)); // 输出 4
// 检查是否至少有一个偶数
boolean containsEven = numbers.stream()
.anyMatch(n -> n % 2 == 0);
System.out.println(containsEven); // 输出 true
4.4 复杂聚合操作
Stream API 还支持许多复杂的聚合操作,例如分组、分区等。
- 分组:可以按某个键将元素分组到
Map
中。 - 分区:将元素分为两个组,通常是基于某个布尔条件。
示例:
List<Person> people = Arrays.asList(
new Person("Alice", 30),
new Person("Bob", 25),
new Person("Charlie", 40)
);
// 分组:按年龄分组
Map<Integer, List<Person>> groupedByAge = people.stream()
.collect(Collectors.groupingBy(Person::getAge));
System.out.println(groupedByAge);
// 分区:按年龄是否大于等于30分区
Map<Boolean, List<Person>> partitionedByAge = people.stream()
.collect(Collectors.partitioningBy(p -> p.getAge() >= 30));
System.out.println(partitionedByAge);
4.5 代码示例
下面是一个综合示例,展示了如何使用 Stream API 的多种特性来处理一个列表:
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public class StreamExample {
public static void main(String[] args) {
List<Person> people = Arrays.asList(
new Person("Alice", 30),
new Person("Bob", 25),
new Person("Charlie", 40),
new Person("David", 30),
new Person("Eve", 20)
);
// 使用并行流
long countOfPeopleOver30 = people.parallelStream()
.filter(person -> person.getAge() > 30)
.count();
System.out.println("Count of people over 30: " + countOfPeopleOver30);
// 使用分组
Map<Integer, List<Person>> groupedByAge = people.stream()
.collect(Collectors.groupingBy(Person::getAge));
System.out.println("Grouped by age: " + groupedByAge);
// 使用分区
Map<Boolean, List<Person>> partitionedByAge = people.stream()
.collect(Collectors.partitioningBy(p -> p.getAge() >= 30));
System.out.println("Partitioned by age >= 30: " + partitionedByAge);
// 使用短路操作
boolean containsPersonNamedAlice = people.stream()
.anyMatch(p -> p.getName().equals("Alice"));
System.out.println("Contains person named Alice: " + containsPersonNamedAlice);
}
}
在这个示例中,我们首先创建了一个 Person
类,并使用 List<Person>
来存储数据。接下来,我们演示了如何使用并行流、分组、分区和短路操作来处理这些数据。这些操作展示了 Stream API 的强大功能和灵活性。
5. 默认方法与静态方法的深入理解
5.1 接口中的默认方法
在 Java 8 中引入了接口中的默认方法,这允许你在接口中定义具有默认实现的方法。这些方法使用 default
关键字定义,并且可以在不强制实现类覆盖这些方法的情况下提供行为。
优点:
- 向后兼容性:可以在不破坏现有实现的情况下为接口添加新方法。
- 行为共享:多个接口可以共享相同的行为实现。
示例:
interface Printable {
default void print() {
System.out.println("Printing...");
}
}
class MyClass implements Printable {
// 不需要实现 print() 方法
}
public class DefaultMethodExample {
public static void main(String[] args) {
MyClass obj = new MyClass();
obj.print(); // 输出 "Printing..."
}
}
5.2 接口中的静态方法
除了默认方法外,Java 8 还允许在接口中定义静态方法。这些方法使用 static
关键字定义,并且不能被实现类继承。它们主要用于提供实用工具方法或工厂方法。
示例:
interface MathOperations {
default double average(double... numbers) {
return Arrays.stream(numbers).average().orElse(0.0);
}
static double max(double... numbers) {
return Arrays.stream(numbers).max().orElse(0.0);
}
}
public class StaticMethodExample {
public static void main(String[] args) {
double maxNumber = MathOperations.max(10.0, 20.0, 30.0);
System.out.println("Max Number: " + maxNumber); // 输出 "Max Number: 30.0"
double avgNumber = MathOperations.average(10.0, 20.0, 30.0);
System.out.println("Average Number: " + avgNumber); // 输出 "Average Number: 20.0"
}
}
5.3 接口中的默认方法与静态方法的使用
1. 默认方法的使用:
- 实现类:实现类可以选择覆盖默认方法,也可以直接使用接口提供的默认实现。
- 多个接口继承冲突:如果一个类实现了多个接口,而这些接口中有相同的默认方法,则需要在实现类中明确指定使用哪一个方法。
示例:
interface A {
default void method() {
System.out.println("Method from A");
}
}
interface B {
default void method() {
System.out.println("Method from B");
}
}
class MyClass implements A, B {
public void method() {
B.super.method(); // 明确指定使用 B 的实现
}
}
public class MultipleInheritanceExample {
public static void main(String[] args) {
MyClass obj = new MyClass();
obj.method(); // 输出 "Method from B"
}
}
2. 静态方法的使用:
- 调用方式:静态方法像普通静态方法一样通过接口名直接调用。
- 作用:静态方法通常用于提供与接口相关的工具方法,例如创建实例、进行计算等。
示例:
interface Factory {
static String createMessage(String text) {
return "Message: " + text;
}
}
public class StaticMethodUsage {
public static void main(String[] args) {
String message = Factory.createMessage("Hello World");
System.out.println(message); // 输出 "Message: Hello World"
}
}
5.4 总结
- 默认方法:在接口中定义的方法,带有默认实现,用于向后兼容旧的实现类,或者为多个实现类提供共享的行为。
- 静态方法:在接口中定义的静态方法,不能被实现类继承,通常用于提供工具方法或工厂方法。
这些特性增强了 Java 接口的功能,并为面向接口编程提供了更多的灵活性。