Java8新特性
前言
为什么要整理这一篇文章?
- Java8是Java 语言开发的一个主要版本。 Oracle 公司于 2014 年 3 月 18 日发布 Java 8 ,其具有许多实用的新特性和改进,如函数式编程,新的 JavaScript 引擎,新的日期 API,新的Stream API 等,而且这些特性在许多实际项目中都有广泛应用。
- 面试中经常会被问到
Java8的主要新特性
- Lambda表达式
- 方法引用
- 默认方法
- 新的编译工具
- Stream API
- Date Time API
- Optional 类
Lambda表达式
- 一个方法作为另一个方法的入参
函数式接口
- 有且仅有一个抽象方法的接口,可以有多个非抽象方法(Java8中的默认方法和静态方法,因为Java8之前接口中的方法都是抽象方法)
- 可以使用**@FunctionalInterface**注解标记函数式接口
- Java8之前的函数式接口:
java.lang.Runnable
java.util.concurrent.Callable
java.security.PrivilegedAction
java.util.Comparator
java.io.FileFilter
java.nio.file.PathMatcher
java.lang.reflect.InvocationHandler
java.beans.PropertyChangeListener
java.awt.event.ActionListener
javax.swing.event.ChangeListener
- Java8新增的函数式接口:java.util.function包下的类,用来支持 Java的 函数式编程
最常用的函数式接口是Function,Supplier,Consumer,Predicate
/**
* Function:apply方法,输入参数并返回结果,
* Funcation第一个参数是apply的入参,第二个参数是apply的返回值
*/
Function<Integer, String> function = new Function<>() {
@Override
public String apply(Integer integer) {
return "数字是:"+integer;
}
};
String apply = function.apply(2);
System.out.println(apply);//数字是:2
/**
* Supplier:get方法,不输入参数并返回结果
* Supplier的参数是get方法的返回值
*/
Supplier<String> supplier = new Supplier<>() {
@Override
public String get() {
return "hahaha";
}
};
System.out.println(supplier.get());//hahaha
/**
* Consumer:accept方法,输入一个参数,没有返回值
* Consumer的参数是accept方法的参数
*/
Consumer<String> consumer = new Consumer<>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
consumer.accept("哈哈哈");//哈哈哈
/**
* Predicate:test方法,输出一个参数,返回值是布尔类型
* Predicate的参数是test方法的参数,用户做断言,判读的
*/
Predicate<String> predicate = new Predicate<>() {
@Override
public boolean test(String s) {
if(s != null && !s.trim().equals("")) {
return true;
}
return false;
}
};
System.out.println(predicate.test(null));//false
匿名内部类
- 一句话说就是没有名字的类
- 匿名内部类是Java中的一种特殊类型的类,它没有名称。通常在需要使用一次性的类或接口实现时使用,例如回调函数、单例模式等。由于它们没有名称,所以在使用后就会被垃圾回收器回收,不会占用额外的内存空间。
- 案例
Runnable就是一个匿名内部类,Runnable是一个接口。
======一般写法
public class MyRunnable implements Runnable {
public void run() {
System.out.println("This is a named inner class running in a thread.");
}
}
Thread thread = new Thread(new MyRunnable());
thread.start();
======匿名内部类写法
new Thread(new Runnable() {
public void run() {
System.out.println("This is an anonymous inner class running in a thread.");
}
}).start();
基本语法
<函数式接口> <变量名> = (参数1,参数2…) -> {
//方法体
}
特点说明
(参数1,参数2…)表示参数列表;->表示连接符;{}内部是方法体
1、=右边的类型会根据左边的函数式接口类型自动推断;
2、如果形参列表为空,只需保留();
3、如果形参只有1个,()可以省略,只需要参数的名称即可;
4、如果执行语句只有1句,且无返回值,{}可以省略,若有返回值,则若想省去{},则必须同时省略return,且执行语句也保证只有1句;
5、形参列表的数据类型会自动推断;
6、lambda不会生成一个单独的内部类文件;
7、lambda表达式若访问了局部变量,则局部变量必须是final的,若是局部变量没有加final关键字,系统会自动添加,此后在修改该局部变量,会报错;
案例
- 接口
public interface MyInterface{
int sum(int num1,int num2);
}
- 实现类
public class MyInterfaceImpl implements MyInterface{
@Override
public int sum(int num1, int num2) {
return num1+num2;
}
}
- 测试类
public static void main(String[] args) {
MyInterfaceImpl mf = new MyInterfaceImpl();
System.out.println(mf.sum(10, 20));
}
匿名内部类的写法
- 不用写实现类
public static void main(String[] args) {
//这部分可以简写成Lambda表达式
MyInterface mf = new MyInterface() {
@Override
public int sum(int num1, int num2) {
return num1+num2;
}
};
System.out.println(mf.sum(10, 20));
}
Lambda写法一:常规写法
- 第一步:将等号右边到方法参数前的内容都删掉
- 第二步:在方法参数和方法体中间加上 ->
- 第三步:去掉多余的 }
MyInterface mf = (int num1, int num2)->{
return num1+num2;
};
Lambda写法二:多个参数且有返回值
- 如果方法体只有一句,{}可以省略
- 方法体只有一句,且是return语句,return 可以省略
- 形参的类型可以省略
- 若形参只有一个,()可以省略,例如
MyInterface mf = num1->num1+10;
MyInterface mf = (num1, num2)->num1+num2;
Lambda写法三:一个参数没有返回值
- 参数只有一个,()可以省略
- 方法体只有一行,{}可以省略
- 接口
public interface MyInterface{
void print(String str);
}
- 测试类
/*匿名内部类写法*/
MyInterface mi = new MyInterface{
@Override
void print(String str){
System.out.println(str);
};
}
/*Lambda常规写法*/
MyInterface mi = (String str)->{
System.out.println(str);
};
/*Lambda简写*/
MyInterface mi2 = str->System.out.println(str);
Lambda写法四:没有参数
- 其他规则一样
- 因为没有参数列表,保证左右的完整性,()不能省略
- 接口
public interface MyInterface{
void test();
}
- 测试类
/*匿名内部类写法*/
MyInterface mi = new MyInterface{
@Override
void test(){
System.out.println("123456");
};
}
//Lambda常规写法
MyInterface mi = ()->{
System.out.println("123456");
};
//简写
MyInterface mi2 = ()-> System.out.println("123456");
Lambda写法总结
- 方法的类型有六种:无参数无返回值,无参数有返回值,多个参数无返回值,多个参数有返回值,一个参数无返回值,一个参数有返回值。
- 将等号后及形参列表前的内容都删掉,形参列表和方法体之间用->连接,去除多余的一个},->左边是形参,右边是方法体
- 形参的类型可省略
- 形参没有,用()表示
- 形参只有一个,()可省略
- 方法体只有一行,{}可省略
- 方法体只有一行,且是return语句,return可省略并且{}可省略
- 其他情况不可省略
Lambda的作用域
- Lambda表达式中使用的局部变量必须是final修饰的,Lambda表达式中的i是被隐性添加了final的,不可更改。
public class Test {
public static void main(String[] args) {
final int i = 9;
MyInterface mf = (num1,num2)->{
System.out.println(i);
return num1+num2;
};
}
}
方法引用
- 一种简化Lambda表达式的方式,它允许你直接引用现有的方法而无需重新定义Lambda函数,主要有四种方法引用。
构造方法引用
- 格式
类名::new
,是对构造方法的引用 - 构造器引用的条件:
1)函数式接口的返回值类型必须是被引用的构造方法对应的类型
2)函数式接口的形参列表必须和被引用的构造方法的形参列表保持一致
3)被引用的构造方法所在类必须有无参构造方法,确保能够通过构造器创建新的实例
//Person类
public class Person {
private int id;
private String name;
public Person(int id, String name) {
this.id = id;
this.name = name;
}
public Person() {
}
}
//创建Person对象的函数式接口
public interface PersonFactory{
Person creatPerson(int id,String name);
}
//使用匿名内部类创建
public static void main(String[] args) {
PersonFactory factory = new PersonFactory() {
@Override
public Person creatPerson(int id, String name) {
return new Person(id,name);
}
};
}
//Lambda简写
PersonFactory factory = (id,name)->new Person(id,name);
//Lambda简写2
PersonFactory factory = Person::new;
静态方法引用
- 格式
类名::静态方法名
,相当于是函数式接口调了一个别人已经写好的静态方法,就是一个中介 - 静态方法引用的条件
1)引用的方法必须是静态方法,是类层面的
2)函数式接口方法的形参列表和静态方法的形参列表保持一致
3)函数式接口方法返回值是void,静态方法的返回值可有可无
4)函数式接口方法如果有返回值,必须和静态方法的返回值保持一致
//函数式接口
public interface PersonFactory{
int parse(String str);
}
//匿名内部类的写法
PersonFactory pf = new PersonFactory() {
@Override
public int parse(String str) {
return Integer.parseInt(str);
}
};
//Lambda常规写法
PersonFactory factory = str->Integer.parseInt(str);
//Lambda静态方法引用
PersonFactory fp = Integer::parseInt;
实例方法引用
- 格式
对象::实例方法名
,函数式接口调用了一个别人写好的实例方法,和静态方法引用不同的是,调用的方法一个是静态方法,一个是实例方法 - 实例方法引用的条件
1)引用的方法是实例方法
2)函数式接口方法的形参列表和引用方法的形参列表保持一致
3)函数式接口方法的返回值是void,被引用的方法返回值可有可无
4)函数式接口方法的如果有返回值,必须和被引用方法的返回值保持一致
//Java8的函数式接口Function
@FunctionalInterface
public interface Function<T,R>{
R apply(T t);
}
//匿名内部类的方式
public static void main(String[] args) {
String str = "hello,world";
Function<String,Boolean> func1 = new Function<String, Boolean>() {
@Override
public Boolean apply(String t) {
return t.endsWith("world");
}
};
boolean test = test(func1,str);
System.out.println(test);
}
public static boolean test(Function<String,Boolean> f,String str){
return f.apply(str);
}
//Lambda表达式
public static void main(String[] args) {
String str = "hello,world";
Function<String,Boolean> func1 = t->str.endsWith("world");
boolean test = test(func1,str);
System.out.println(test);
}
public static boolean test(Function<String,Boolean> f,String str){
return f.apply(str);
}
//Lambda表达式简写
public static void main(String[] args) {
String str = "hello,world";
Function<String,Boolean> func1 = str::endsWith;
boolean test = test(func1,str);
System.out.println(test);
}
public static boolean test(Function<String,Boolean> f,String str){
return f.apply(str);
}
特殊方法(对象方法)引用
- 格式
类名::实例方法名
,函数式接口中形参的第一个参数是实例方法的调用方 - 对象方法引用的条件
1)函数式接口中形参的第一个参数是实例方法的调用方
2)函数式接口方法的形参列表和引用方法的形参列表保持一致
3)函数式接口方法的返回值是void,被引用的方法返回值可有可无
4)函数式接口方法的如果有返回值,必须和被引用方法的返回值保持一致
public Class Student{
private String name;
public Student(){
}
public Student(String name){
this.name = name;
}
public String getName(){
return this.name;
}
public String setName(String name){
this.name = name;
}
}
public class Test{
public static void main(String[] args) {
//匿名内部类写法
Function<Student,String> fu = new Function<>(){
@Override
public String apply(Student student){
return stduent.getName();
}
}
//Lambda表达式写法
Function<Student,String> fu = student -> stduent.getName();
//Lambda表达式简写
Function<Student,String> fu = Student::getName();
}
}
Stream流
- Java 8 API添加了一个新的抽象称为流Stream,可以让你以一种声明的方式处理数据。
- 极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码
- 数据源可以是集合,数组,I/O channel(nio new IO非阻塞式IO), 产生器generator 等。
- 你可以将stream流看成一个工厂的流水线,流水线都是上一步操作的结果交给下一个工序继续完成,其实stream流也是,他的中间操作的返回值都是操作对象本身,在这里你是不是联想到了链式编程,其实他们的思想都是一样的。
stream流的中间操作和终端操作
- 中间操作(Intermediate Operations):操作的返回值还是一个stream;不会立即执行,是惰性的。
常见的方法有:
1)filter(Predicate<? super T> predicate): 过滤
2)map(Function<? super T,? extends U> mapper): 将Stream中的元素应用给定的函数,并返回结果。(简单说就是修改)
3)sorted(Comparator<? super T> comparator): 排序
4)distinct():去重
5)limit(long maxSize): 取前几个,如果maxSize大于stream的长度,不会报错,会全部取出
6)skip(long n): 跳过前n个元素。
7)peek(Consumer<? super T> action): 对Stream中的每个元素执行给定的操作。 - 结束操作(Terminal Operations):返回一个结果或者副作用;会立刻执行;会触发中间操作的执行,并且只执行一次
常见的方法有:
1)collect(Collector<? super T,A,R> collector): 转集合,除了map
2)forEach(Consumer<? super T> action): 遍历
3)reduce(BinaryOperator accumulator): 二元操作符规约
4)count(): 计数
5)findFirst(): 返回第一个元素(如果存在)。
6)anyMatch(Predicate<? super T> predicate): 任意一个元素匹配,并返回true或false。
7)allMatch(Predicate<? super T> predicate): 所有元素匹配,并返回true或false。
8)noneMatch(Predicate<? super T> predicate): 无元素匹配,并返回true或false。
9)toArray(): 转数组
10)toCollection(Supplier<? extends Collection> supplier): 转集合
生成流
- Java8中集合有两个接口生成流
1)stream() − 为集合创建串行流
2)parallelStream() − 为集合创建并行流(一般不用) - 串行流和并行流谁的效率更高?
取决于数据量,听起来并行的效率要比串行的效率高,但是并行流有创建流和合并流的消耗,所以数据量小的情况下串行流的效率更高,反之并行流的效率更高。 - 串行流获取数据是按照顺序来的,并行流则是随机的
数组和集合获取stream流
- 数组
//方式1,用Stream.of(),参数是可变参数,他的底层是Arrays.stream(可变参数)
Stream<Integer> integerStream = Stream.of(1,2,3);
//方式2,Arrays.stream(int数组)
IntStream stream = Arrays.stream(new int[]{1, 2, 3});
//Stream和IntStream是同级的,都是BaseStream的子类,IntStream有更多的方法,他的参数只能是int类型的,Stream任意类型
- 集合
//集合对象.stream(),list和set就直接用对象.stream(),
List<String> list = Arrays.asList("你", "好", "吗");//将可变参数转为集合
Stream<String> stream1 = list.stream();
//map只能将entrySet转为stream,Entry的第一个参数是key,第二个参数是value
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
map.put("orange", 3);
// 将Map转换为Stream
Stream<Map.Entry<String, Integer>> stream2 = map.entrySet().stream();
Stream流的常见操作案例
先准备一个简单的员工类
public class Employee {
private String name;//姓名
private Integer age;//年龄
private BigDecimal salary;//工资
public String getName() {
return name;
}
//因为要链式编程,所以将默认的void改成了对象
public Employee setName(String name) {
this.name = name;
return this;
}
public Integer getAge() {
return age;
}
public Employee setAge(Integer age) {
this.age = age;
return this;
}
public BigDecimal getSalary() {
return salary;
}
public Employee setSalary(BigDecimal salary) {
this.salary = salary;
return this;
}
public Employee() {}
public Employee(String name, int age, BigDecimal salary) {
this.name = name;
this.age = age;
this.salary = salary;
}
@Override
public String toString() {
return "姓名:" + name + " 年龄:" + age + " 工资:" + salary;
}
}
- filter过滤
List<Employee> list = Arrays.asList(
new Employee("哈哈哈",19,new BigDecimal("8000")),
new Employee("嘻嘻嘻",20,new BigDecimal("9000")),
new Employee("呵呵呵",19,new BigDecimal("12000")),
new Employee("嘿嘿嘿",24,new BigDecimal("10000")));
//找出集合中,年龄大于20,工资大于10000的人(传统写法)
for (Employee employee : list) {
if(employee.getAge() > 20 && employee.getSalary().compareTo(new BigDecimal("10000")) > 0){
System.out.println(employee);
}
}
//过滤出年龄大于20并且工资大于10000的员工的信息(filter写法)
Stream<Employee> es = list.stream().filter(new Predicate<Employee>() {
@Override
//test的结果为true表示满足,反之不满足
public boolean test(Employee employee) {
//这个打印语句是不会执行的,因为filter是一个中间操作,只有遇到终端操作时才会执行
System.out.println(employee);
return employee.getAge() >= 20 && employee.getSalary().compareTo(BigDecimal.valueOf(10000)) > 0;
}
});
//要想打印出过滤后的结果,得执行终端操作,使用forEach遍历打印。
es.forEach(System.out::println);
- limit
//取前两个
Stream<Employee> limit = list.stream().limit(2);
//limit同样是一个中间操作,所以要使用终端操作才能显示结果
limit.forEach(System.out::println);
- map(可以修改数据)
//需求就是只返回员工的姓名
Stream<String> map = list.stream().map(new Function<Employee, String>() {//第一个是传进来的参数是方法的形参,第二个传进来的参数是返回值类型
@Override
public String apply(Employee employee) {
//返回的结果就是你想要得到的结果
return employee.getName();
}
});
//map同样是一个中间操作
map.forEach(System.out::println);
- sorted
排序,其实他有两个重载的方法,一个是无参的,一个是参数为比较器的,无参的适用于那些实现过自然排序的,Java的哪些包装类都实现了自然排序,但是都是按照升序的,如果无法满足你的需求,就可以自己在比较器中定义规则。
//按照年龄降序排列
list.stream().sorted(new Comparator<Employee>() {
@Override
public int compare(Employee o1, Employee o2) {
return o2.getAge()-o1.getAge();
}
}).forEach(System.out::println);
- collect
聚合操作,前面的例子都是中间操作,collect是终端操作。配合Collectors工具类使用
//转List
List<Employee> collect = list.stream().collect(Collectors.toList());
//转Set
Set<Employee> collect1 = list.stream().collect(Collectors.toSet());
//转Map,匿名内部类写法,键是姓名,值是Employee对象
Map<String, Object> collect2 = list.stream().collect(Collectors.toMap(new Function<Employee, String>() {
@Override
public String apply(Employee employee) {
return employee.getName();
}
}, new Function<Employee, Object>() {
@Override
public Object apply(Employee employee) {
return employee;
}
}));
//转Map简单lambda写法,e ->e 的变量任意,两边相同就可,其实就是传什么参数进来,返回了什么参数
Map<String, Object> collect2 = list.stream().collect(Collectors.toMap(Employee::getName,e ->e));
//转LinkedList,没有直接提供方法的Collection接口下的集合都可以用toCollection
LinkedList<Employee> collect3 = list.stream().collect(Collectors.toCollection(new Supplier<LinkedList<Employee>>() {
@Override
public LinkedList<Employee> get() {
return new LinkedList<>(); //转什么就返回一个什么
}
}));
//求个数
Long collect4 = list.stream().collect(Collectors.counting());
//按照年龄分组
Map<Integer, List<Employee>> collect5 = list.stream().collect(Collectors.groupingBy(new Function<Employee, Integer>() {
@Override
public Integer apply(Employee employee) {
return employee.getAge();
}
}));
//求年龄的平均值:averagingInt、averagingDouble、averagingLong
Double collect6 = list.stream().collect(Collectors.averagingInt(new ToIntFunction<Employee3>() {
@Override
public int applyAsInt(Employee3 employee3) {
return employee3.getAge();
}
}));
//用的不多,(这个例子就是所有员工的年龄相加)
Integer collect8 = list.stream().collect(Collectors.reducing(0, new Function<Employee3, Integer>() {
@Override
public Integer apply(Employee3 employee3) {
return employee3.getAge();
}
},
new BinaryOperator<Integer>() {
@Override
public Integer apply(Integer integer, Integer integer2) {
return integer + integer2;
}
}));
System.out.println(collect8);
//字符串拼接,只能用于实现了CharSequence的泛型
List<String> list = Arrays.asList("A", "B", "C", "D");
//无参,就是将所有内容连接起来
String result = list.stream().collect(Collectors.joining());
System.out.println(result); // 输出:ABCD
//一个参数:连接符
String result1 = list.stream().collect(Collectors.joining("-"));
System.out.println(result1); // 输出:A-B-C-D
//三个参数:连接符,前缀,后缀
String result2 = list.stream().collect(Collectors.joining("-", "[", "]"));
System.out.println(result2); // 输出:[A-B-C-D]
Optional容器
- Java8用来避免空指针异常的,其实没啥用;是一个容器对象
String s = "123";
//把对象装进Optional容器,如果对象是null,则直接抛出空指针异常
Optional<String> s2 = Optional.of(s);
//把对象装进Optional容器,如果对象是null也不报错
Optional<String> s1 = Optional.ofNullable(s);
//从Optional中获取对象,如果对象为空,抛出NoSuchElementException异常
System.out.println(s1.get());
//判断optional中存放的对象是否为null,不是空就返回true,是空就返回false
System.out.println(s1.isPresent());
//先判断,再取
if(s1.isPresent()){
System.out.println(s1.get());
}
//获取optional中的对象,如果是null,可以设置默认值
System.out.println(s1.orElse("对象是null"));
//获取optional中的对象,也可以设置默认值,但是可以做更多的是,可以优化lambda表达式
s1.orElseGet(new Supplier<String>() {
@Override
public String get() {
//此处还能写其他的业务代码
System.out.println("hahahah");
//如果是null,可以返回默认值
return "对象是null";
}
});
//创建一个空的Optional实例
Optional<Object> empty = Optional.empty();
System.out.println(empty);//Optional.empty
//map:改变把Optional对象的值,如果原对象值为null,改变不了
Optional<Object> o = s2.map(new Function<String, Object>() {
@Override
public Object apply(String s) {
return "456";
}
});
System.out.println(o.get());//456
//flatMap:和map一样,但是返回值必须是Optional
Optional<Object> o = s2.flatMap(new Function<String, Optional<?>>() {
@Override
public Optional<?> apply(String s) {
return Optional.of(s);
}
});
Java8日期类
- Date是线程不安全的,LocalDate是线程安全的
- LocalDate 可以指定获取日期,日期时间,时间
- LocalDate 可以根据时区,获取指定时区时间,支持国际化
- Date在很多包下都有,容易导包错误
- Java8提供的日期类都在java.time包下
//获取当前的日期
LocalDate now= LocalDate.now();
//获取当前日期+时间
LocalDateTime now1 = LocalDateTime.now();
//获取时间
LocalTime now2 = LocalTime.now();
System.out.println(now);//2024-02-04
System.out.println(now1);//2024-02-04T01:04:12.585509300
System.out.println(now2);//01:04:12.585509300
//localdate日期转换
DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-DD HH:mm:ss");
System.out.println(df.format(now1));//2024-02-35 01:04:12只能对now1,只有now1中包含年月日时分秒
//时区类,获取所有的ZoneId
ZoneId.getAvailableZoneIds().forEach(System.out::println);
//通过字符串时区获取ZoneId
ZoneId id = ZoneId.of("Asia/Chongqing");
//获取指定时区的时间
LocalDateTime now3 = LocalDateTime.now(id);
System.out.println(now3);//2024-02-04T01:04:12.597477100