1. 接口的默认方法
在Java 8中,接口引入了一种新的特性------默认方法(Default Method)。默认方法是指在接口中定义有具体实现的方法,它为接口增加了行为的同时,又不会影响现有的实现此接口的类。这种设计允许我们在不影响已有的实现类的情况下,为接口添加新的方法。
默认方法的语法格式如下:
Java
public interface MyInterface {
// 默认方法
default void myMethod() {
System.out.println("This is a default method in the interface.");
}
}
在实现这个接口的类中,可以选择覆盖默认方法,也可以直接继承默认实现:
Java
public class MyClass implements MyInterface {
// 不覆盖默认方法时,直接继承接口的实现
}
// 或者覆盖默认方法
public class AnotherClass implements MyInterface {
@Override
public void myMethod() {
System.out.println("This is an overridden default method.");
}
}
默认方法有助于解决接口演化时可能出现的向后兼容性问题,特别是在大型项目中,有许多类已经实现了某个接口时,新增接口方法的需求变得更为安全和方便。同时,接口默认方法还可以用来提供一种通用的默认行为,实现类可以根据需要选择是否重写。
2. 函数式接口
函数式接口在Java中是指只有一个抽象方法的接口。这种接口特别适用于Lambda表达式的使用,因为Lambda表达式就是用来代表一个匿名函数,而函数式接口正好定义了一个单一的函数描述符,这二者结合可以简化代码,支持函数式编程风格。
在Java 8中,为了更好地支持Lambda表达式,引入了一个特殊的注解@FunctionalInterface
,尽管并非强制要求,但使用这个注解可以确保接口确实只有一个抽象方法,编译器将会对此进行检查,如果接口中有多个抽象方法,就会抛出编译错误。
例如,Java 8标准库中的java.util.function
包中包含了一系列预定义的函数式接口,如:
Predicate<T>
:接受一个参数并返回一个布尔值。Function<T, R>
:接受一个参数并返回一个结果。Consumer<T>
:接受一个参数,无返回值,仅用于消费或处理数据。Supplier<T>
:无参数,返回一个结果。UnaryOperator<T>
:接收一个参数并返回同类型的值,相当于Function<T, T>的特例。BiFunction<T, U, R>
:接收两个参数并返回一个结果。BiConsumer<T, U>
:接收两个参数,无返回值。- 等等...
例如,一个使用Function
接口和Lambda表达式的例子:
Java
import java.util.function.Function;
public class Example {
public static void main(String[] args) {
Function<Integer, String> stringConverter = (integer) -> Integer.toString(integer);
System.out.println(stringConverter.apply(123)); // 输出:"123"
}
}
在这个例子中,Function<Integer, String>
是一个函数式接口,它定义了一个从Integer到String的转换方法。Lambda表达式(integer) -> Integer.toString(integer)
实现了这个接口,它接收一个整数参数并返回其字符串表示形式。
3. Lambda 表达式
Lambda 表达式是Java 8引入的一项重大特性,它极大地增强了Java在函数式编程方面的表现力。Lambda表达式允许程序员以一种简洁的形式定义匿名函数,这些函数可以在需要函数作为参数或返回值的上下文中使用,例如在集合的流(Stream)操作、事件监听、函数式接口等方面。
在Java中,Lambda表达式的典型语法格式如下:
Java
(parameters) -> {
// function body
}
这里的组成部分说明如下:
- Parameters:括号内列出零个、一个或多个参数,参数类型可以省略,如果编译器可以从上下文推断出来的话。
- 箭头符号:-> 表示参数列表与函数体之间的分隔符。
- Function Body:函数体可以是一个表达式或一个代码块,如果是表达式并且仅有一行,则可以省略花括号;如果有多个语句则需要用花括号包围起来。
例如:
Java
(int x, int y) -> x + y; // 返回两个整数之和的Lambda表达式,没有显式声明类型,编译器会根据上下文推断类型
() -> System.out.println("Hello, Lambda!"); // 无参Lambda表达式,打印一句话
(x) -> { if (x > 0) return true; else return false; } // 带有单个参数和完整代码块的Lambda表达式
Lambda表达式通常与函数式接口配合使用,函数式接口是指只有一个抽象方法的接口,如Java.util.function包中定义的一系列函数式接口(如Predicate、Function、Consumer等)。
Lambda表达式的引入,使得Java开发者可以编写更加简洁、可读性更高的代码,并且更好地支持并行处理和函数式编程范式。
4. stream流式计算
Java 8 中引入的 Stream
API 是一套强大的流式计算工具,它提供了对集合、数组和其他数据源中的数据进行高效、声明式和并行处理的能力。流式计算是一种处理数据集合的方式,允许开发者通过一系列中间操作和终端操作构建流水线式的处理逻辑。
基本概念:
- Stream: Stream 是一个可以从数据源获取元素的序列,它并不一定在内存中全部存储数据,而是按需生成或消费数据。数据源可以是集合、数组、I/O通道,甚至是生成器等。
- 数据源 : 创建一个流需要一个数据源,例如
Arrays.asList(1, 2, 3).stream()
从一个列表创建一个整数流。 - 中间操作 : 中间操作如
filter
,map
,sorted
,distinct
等,它们会返回一个新的流,原始流保持不变。中间操作是惰性的,只有当终端操作执行时才会真正计算。 - 终端操作 : 终端操作如
collect
,count
,forEach
,reduce
等,它们会导致流的计算并得出结果。执行终端操作后,流就被消费掉了,不能再次被使用。
示例:
Java
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
// 中间操作:过滤出偶数,映射为平方数
Stream<Integer> squareOfEvenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * n);
// 终端操作:收集结果到新的列表
List<Integer> squaredEvenNumbers = squareOfEvenNumbers.collect(Collectors.toList());
并行流 : Java 8 Stream API 还支持并行计算,通过 .parallel()
方法可以将流转化为并行流,充分利用多核处理器的优势。并行流会自动分割数据集,分配给多个线程同时处理,然后再合并结果。
注意事项 : Stream API 强调了函数式编程思想,操作之间不应有副作用,也就是说,在处理流的过程中不应该改变外部状态,这样有利于并发安全和正确性。另外,Stream API 不仅仅适用于小型数据集,对于大规模数据处理也能发挥很好的作用,尤其是通过并行流进行处理时。
总结: Java 8 Stream 流式计算极大地提高了处理数据的灵活性和效率,通过组合不同的中间和终端操作,可以简洁地表达复杂的计算逻辑,同时便于实现高性能的并行计算。
5. 方法与构造函数引用
在Java 8中,方法引用(Method Reference)和构造器引用(Constructor Reference)是lambda表达式的两种高级形式,它们提供了更为简洁的语法来引用已有方法或构造器的功能。
方法引用: 方法引用允许你直接引用现有方法的名称,代替书写lambda表达式。方法引用分为以下几种形式:
-
静态方法引用 : 当需要引用一个静态方法时,格式为
Class::staticMethodName
。例如,如果你有一个Comparator接口的实现,可以用Integer类的compare静态方法代替lambda表达式:Java1Comparator<Integer> byValue = Integer::compareTo;
-
实例方法引用 : 如果需要引用一个对象实例的方法,并且该方法的接受者是已知的,格式为
instance::methodName
。例如:JavaPerson person = new Person(); Predicate<String> isNameJohn = person::getName; // 假设Person类有个getName方法
-
特定类的任意实例的方法引用 : 当方法的接受者是方法调用上下文的一部分时,格式为
ClassName::methodName
。例如,在Collections.sort()方法中,传入一个方法引用来比较对象:JavaList<Person> people = ...; Collections.sort(people, Person::compareToByName); // 假设有compareToByName方法
-
超类方法引用 : 引用一个超类的默认方法或静态方法,格式为
SuperType::methodName
。 -
数组方法引用 : 引用数组的默认方法,如
Array::sort
。
构造器引用 : 构造器引用是用来引用类的构造器,用来代替新建实例的lambda表达式。构造器引用的格式为 ClassName::new
。当需要创建特定类型的对象作为函数式接口的实现时,构造器引用非常有用。例如:
Java
Function<String, Person> stringToPerson = Person::new; // 假设Person有一个String参数的构造器
String name = "John Doe";
Person john = stringToPerson.apply(name); // 使用构造器引用创建Person实例
这两种引用方式都可以简化代码,使代码更加清晰,同时鼓励程序员复用已存在的方法逻辑。通过方法引用和构造器引用,Java 8增强了对函数式编程的支持,使得代码更加简洁且易于理解。
6. Date API
Java 8 引入了一套全新的日期和时间API,位于java.time
包下,这套API被设计为取代旧版的java.util.Date
和java.util.Calendar
等类,解决了老API在处理日期和时间时的一些常见痛点,如线程安全问题、设计复杂不易用、缺乏对ISO-8601标准的良好支持以及无法精确处理纳秒级时间等。
以下是Java 8新版日期和时间API中几个关键类的简介:
- LocalDate: 表示仅包含日期(年、月、日)的不可变对象,不包含时间信息,也没有时区关联。
- LocalTime: 表示仅包含时间(小时、分钟、秒和纳秒)的不可变对象,同样不包含日期和时区信息。
- LocalDateTime : 结合了
LocalDate
和LocalTime
,表示日期和时间,但不包含任何时区信息。 - ZonedDateTime: 表示包含日期、时间以及明确时区信息的完整日期时间。
- Instant: 表示时间戳,它是UTC时间轴上的一个瞬时点,包含日期和时间信息,精确到纳秒。
- Duration 和 Period :
Duration
表示持续时间,以秒和纳秒为单位;Period
表示时间段,以年、月、日为单位。 - Clock: 提供访问当前即时、日期和时间(考虑到时区)的方法。
- TemporalAdjuster 和 TemporalAdjusters: 提供调整日期时间对象的工具,如获取下一个工作日、月初、年末等。
- MonthDay , YearMonth 和 Year: 用于表示不完整的日期,例如只包含月份和天数,或者只包含年份和月份。
- ChronoUnit 和 ChronoField: 提供处理时间单位和字段的标准枚举类。
Java 8的新日期时间API不仅设计得更为直观和易用,而且大部分类都是不可变的,有利于线程安全,还提供了丰富的运算和格式化方法,支持流畅的链式调用,以及更好的并发性能和扩展性。同时,这套API还支持时区处理、闰秒调整和与其他日期时间格式规范之间的转换。
7. 重复注解
在Java 8之前,一个注解在同一程序元素上只能使用一次,如果你想在一个地方多次应用相同的注解,就需要采取一些额外的技术手段,例如创建一个容器注解来包裹多个同类型的注解。
Java 8引入了**重复注解(Repeating Annotations)**这一特性,允许在同一个程序元素上多次使用相同的注解类型,提高了代码的可读性和简洁性。为了实现这一点,Java 8引入了@Repeatable
元注解,它用于声明某个注解类型是可以重复的。
如何使用重复注解:
-
定义重复注解 : 首先,需要定义一个可重复的注解类型,并且为其创建一个容器注解。
Java// 可重复注解 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Repeatable(Roles.class) // 使用@Repeatable声明这个注解是可以重复的 public @interface Role { String value(); } // 容器注解 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface Roles { Role[] value(); }
-
使用重复注解 : 现在可以在类或方法上多次应用
@Role
注解,它们会被自动收集到@Roles
容器注解中。Java@Role("admin") @Role("moderator") @Role("user") public class UserAccount { // ... } // 相当于 @Roles({@Role("admin"), @Role("moderator"), @Role("user")}) public class UserAccount { // ... }
-
访问重复注解 : 在运行时,可以通过反射API来访问这些重复注解,就像访问普通的注解一样,只不过现在容器注解
@Roles
的value
是一个Role
数组,可以遍历得到所有的Role
注解。JavaClass<UserAccount> clazz = UserAccount.class; Roles roles = clazz.getAnnotation(Roles.class); if (roles != null) { for (Role role : roles.value()) { System.out.println(role.value()); } }
这样,Java 8的重复注解功能就允许开发者更加灵活和方便地在同一个程序元素上多次使用同一个注解类型。