1、Java 8 新特性简介
主要内容
- Lambda表达式
- 函数式接口
- 方法引用与构造器引用
- Stream API
- 接口中的默认方法与静态方法
- 新时间日期API
- 其他新特性
特点
- 速度更快。对于底层的数据结构做了更新与改动,垃圾回收机制内存结构做了一定的改变。
- 代码更少(增加了新的语法Lambda表达式)
- 强大的Stream API
- 便于并行
- 最大化减少空指针异常 Optional
其中最为核心的就是lambda表达式与Stream API
底层数据结构的更改
1、对于顶层数据结构最核心的体现,HashMap。
原来的HashMap数据存储的结构 数组 + 链表
HashMap底层实际上是一个16位的数组,数组的值为entry。存储对象时先通过hash算法计算出对象所对应的数组索引值,然后在通过equals方法比较该索引下是否已有相同的对象,有则覆盖原来的对象。没有则放在原有对象的前面,与原来的对象形成一个链表,这种情况我们称之为碰撞。碰撞会使我们存储的效率变低,我们需要重写hashCode与equals方法来尽量避免这种碰撞。当HashMap中元素存储的数量过大,会降低我们的存储的效率,HashMap为其提供加载因子。当元素到达现有Hash表的75%的时候,即会对其进行扩容。原来的元素就会进行重新运算,存储到扩容后的空间内,这样就减小了元素碰撞的几率。
Java 8以后HashMap数据存储的结构 数组 + 链表 + 红黑树
Java8以后在原来的存储结构下增加了红黑树的结构,我们知道在元素发生碰撞,即多个元素被存储到一个索引下时会时会形成链表。当链表元素的数量到8且整个HashMap存储的元素数量的大于64时,会将原来的链表转换为红黑树。红树。在添加时会将元素进行比较,并将较大的元素添加到元素的左边。这种结构增强了除添加以外的其它所有操作的效率。且在Hash表扩容时,原来的元素不在不需要经过复杂的排序。只需要找原来Hash表的总长度,加上它当前所在的索引位置。
2、Lambda表达式
Lambda是一个匿名函数,我们可以将Lambda表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)。可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到提升。
先看看以下示例来感受一下Java8的Lambda表达式与其他新特性的独特之处吧
public class TestLambda {
//原来的匿名内部类
@Test
public void t1() {
Comparator<Integer> comparator = new Comparator<Integer>() {
public int compare(Integer o1, Integer o2) {
return Integer.compare(o1,o2);
}
};
}
//Lambda表达式
@Test
public void t2() {
Comparator<Integer> com = (x,y) -> Integer.compare(x,y);//简化了函数式接口的实现
}
List<Emp> empList = asList(
new Emp("张三", 18, 100.00),
new Emp("李四", 20, 200.00),
new Emp("王五", 24, 300.00),
new Emp("赵六", 14, 400.00)
);
//需求:获取年龄大于等于18的员工
//定义过滤方法
public List<Emp> filterEmployee(List<Emp> list,MyPredicate<Emp> mp){
List<Emp> emps = new ArrayList<Emp>();
for (Emp emp : empList) {
if(mp.test(emp)){
emps.add(emp);
}
}
return emps;
}
@Test
public void t3() {
System.out.println("实现方案一:接口实现类");
List<Emp> emps = filterEmployee(empList,new FilterEmployeeByAge());
for (Emp emp : emps) {
System.out.println(emp);
}
System.out.println("实现方案二:匿名内部类");
List<Emp> emps1 = filterEmployee(empList, new MyPredicate<Emp>() {
@Override
public boolean test(Emp emp) {
return emp.getAge() >= 18;
}
});
for (Emp emp : emps1) {
System.out.println(emp);
}
System.out.println("实现方案三:lambda表达式");
List<Emp> emps2 = filterEmployee(empList, (e) -> e.getAge() >= 18);
emps2.forEach(System.out::println);
System.out.println("实现方案四:Stream API");
empList.stream()
.filter((e)->e.getAge()>=19)
.limit(2)
.forEach(System.out::println);
empList.stream()
.map(Emp::getName)
.forEach(System.out::println);
}
}
public interface MyPredicate<T> {
boolean test(T t);
}
public class FilterEmployeeByAge implements MyPredicate<Emp>{
@Override
public boolean test(Emp emp) {
return emp.getAge() >= 18;
}
}
以上示例是不是让你感受到Lambda表达式与Stream API的强大,想要继续向下了解,接下来讲解的是Lambda表达式的相关知识。
Lambda表达式的基础语法:Java8中引入了一个新的操作符"->"该操作符称为箭头操作符或Lambda操作符,箭头操作符将Lambda表达式分为两部分:
左侧:Lambda表达式的参数列表。
右侧:Lambda表达式中所需执行的功能,即Lambda体
//语法格式一:无参无返回值
() -> System.out.println("Hello Lambda");
//语法格式二:有一个参数,无返回值
(x) -> System.out.println(x);
//语法格式三:若只有一个参数,小括号可以省略
x -> System.out.println(x);
//语法格式四:有两个以上的参数,有返回值,且Lambda中有多条语句
Comparator<Integer> com = (x, y) -> {
System.out.println("函数式接口");
return Integer.compare(x,y);
};
//语法格式五:若Lambda体中只有一条语句,return和大括号都可以省略
Comparator<Integer> com = (x, y) -> return Integer.compare(x,y);
//语法格式六:Lambda表达式的参数列表的数据类型都可以不写,因为JVM编译器通过上下文推断出,数据类型,即"类型推断"
(Integer x,Integer y) -> Integer.compare(x,y);
Lambda函数式接口
Lambda表达式需要函数式接口的支持
函数式接口:接口中只有一个抽象方法的接口,称为函数式接口。根据规范需要使用@FuncatinalInterface修饰函数式接口,该接口可以用于检查接口是否是函数式接口。
Java8 内置的四大核心函数式接口
Consumer<T>:消费型接口
void accept(T t);
Supplier<T>:供给型接口
T get();
Function<T>:函数型接口
R apply(T t);
Predicate<T>:断言型接口
boolean test(T t);
其他接口
方法引用与构造器引用
1、方法引用:若Lambda体中的内容已经有别的方法实现了,我们可以使用"方法引用"引用已有的方法。
//方法引用主要的三种语法格式:
//对象::实例方法名
Consumer<String> sup = System.out::println;
//类::静态方法名
Comparator<Integer> sup2 = Integer::compare;
//类::实例方法名
BiPredicate<String,String> b = String::equals;
方法引用的规则:
(1)Lambda体中调用方法的参数列表与返回值类要与函数式接口中抽象方法的函数列表和返回值类型保持一致。
(2)若Lambda参数列表中的第一参数是实例方法的调用者,而第二参数是实例方法的参数时,可以使用ClassName::method。
2、构造器引用
//对象构造器引用 语法:ClassName::new;
Supplier<Emp> sup = Emp::new;
//数组构造器引用 语法:Type[]::new;
Function<Integer,String[]> fun = String[]::new;
注意:使用构造器引用时构造器参数列表要符合函数式接口抽象方法的参数列表保持一致。
3、Stream API
了解Stream
Java8中两个最为重要的改变一个是Lambda表达式,一个就是Stream API(java.util.stream.*)了。
Stream是Java8中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用Stream API对集合数据进行操作,就类似与使用SQL执行的数据库查询。也可以使用Stream API来并行执行操作。简而言之,Stream API提供了一种高效且易于使用的处理数据的方式。
Stream流到底是什么,其实它是一种数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。集合是元素,流的作用就是对元素进行计算。
Stream流使用时注意事项:
(1)Stream自己不会存储元素。
(2)Stream不会改变源对象。相反,他们会返回一个持有结果的新Stream。
(3)Stream操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。
Stream使用步骤:创建(获取流) >>> 中间操作(处理流) >>> 终止操作(执行流)
创建Stream流
1.通过Collection 系列集合提供的stream()或parallelStream()
List<String> list = new ArrayList<String>();
Stream<String> stream1 = list.stream();
2.通过Arrays 中的静态方法stream()获取数组流
Emp[] emps = new Emp[10];
Stream<Emp> stream2 = Arrays.stream(emps);
3.通过Stream类中的静态方法 of()
Stream<String> stream3 = Stream.of("aa", "bb", "cc");
4.创建无限流
Stream<Integer> stream4 = Stream.iterate(0, x -> x + 2);
stream4.limit(10).forEach(System.out::println);
//生成
Stream.generate(()->Math.random())
.forEach(System.out::println);
Stream的中间操作
多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作是不会执行任何的处理的。而在终止操作时一次性处理全部,称为“惰性求值”。
筛选与切面操作
方法 | 描述 |
---|---|
filter(Predicate p) | 提供一个断言型接口实现,获取返回值为ture的元素 |
distiinct() | 筛选,通过流所生成元素的hashCode()与equals()方法去除重复元素 |
limit(long maxSize) | 截断流,使其元素不超过给定数量。 |
skip(long n) | 跳过元素,返回一个扔掉的前n个元素的流。若流中元素不足n个,则返回一个空流,与limit(n)互补 |
排序
方法 | 描述 |
---|---|
sorted() | 产生一个新流,其中按自然顺序排序 |
sorted(Comparator com) | 产生一个新流,其中按比较器顺序排序 |
示例:
public void t2() {
int[] arr = {4,3,5,6};
Arrays.stream(arr)
.sorted()
.forEach(System.out::println);
List<Emp> empList = Arrays.asList(
new Emp("张三", 18, 100.00),
new Emp("李四", 20, 200.00),
new Emp("王五", 24, 500.00),
new Emp("王五", 24, 600.00),
new Emp("赵六", 14, 400.00)
);
Stream<Emp> sorted = empList.stream()
.sorted((e1, e2) -> {
if (e1.getAge().equals(e2.getAge())) {
return e1.getSalary().compareTo(e2.getSalary());
}
return e1.getAge().compareTo(e2.getAge());
});
sorted.forEach(System.out::println);
}
映射
方法 | 描述 |
---|---|
map(Funcation f) | 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。 |
mapToDouble(ToDoubleFunction f) | 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的DoubleStream。 |
mapToInt(ToIntFunction f) | 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的IntStream。 |
mapToLong(ToLongFunction f) | 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的LongStream。 |
flatMap(Function f) | 接收一个函数作为参数,将流中的每一个值都转换为另一个流,并将所有流连接成一个流。 |
Stream的终止操作
查找与匹配
方法 | 描述 |
---|---|
allMatch | 检查是否匹配所有元素 |
anyMatch | 检查是否至少匹配一个元素 |
noneMatch | 检查是否没有匹配的元素 |
findFirst | 返回第一个元素 |
findAny | 返回当前流中的任意元素 |
count | 返回流中元素的总个数 |
max | 返回流中最大值 |
min | 返回六中最小值 |
forEach(Consumer c) | 内部迭代(使用Collection接口需要用户去做迭代,称为外部迭代。相反,Stream API使用内部迭代——它帮你把迭代做了) |
示例:
@Test
public void t3() {
List<Emp> empList = Arrays.asList(
new Emp("张三", 18, 100.00,Status.FREE),
new Emp("李四", 20, 200.00,Status.BUSY),
new Emp("王五", 24, 500.00,Status.VOCATION),
new Emp("王五", 24, 600.00,Status.FREE),
new Emp("赵六", 14, 400.00,Status.BUSY)
);
boolean b = empList.stream()
.noneMatch(e -> e.getStatus().equals(Status.VOCATION));
System.out.println(b);
}
归纳(汇总)
方法 | 描述 |
---|---|
reduce(T iden,BinaryOperator b) | 可将流中的元素反复结合,得到一个值,返回T |
reduce(BinaryOperator b) | 可以将流中元素反复结合起来,得到一个值。返回Optional<T> |
备注:map与reduce的连接通常称为map-reduce模式,因Google用它来进行网络搜素而出名。
示例:
@Test
public void t4() {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
Integer sum = list.stream()
.reduce(0, Integer::sum);
System.out.println(sum);
List<Emp> empList = Arrays.asList(
new Emp("张三", 18, 100.00,Status.FREE),
new Emp("李四", 20, 200.00,Status.BUSY),
new Emp("王五", 24, 500.00,Status.VOCATION),
new Emp("王五", 24, 600.00,Status.FREE),
new Emp("赵六", 14, 400.00,Status.BUSY)
);
Optional<Double> op = empList.stream()
.map(Emp::getSalary)
.reduce(Double::sum);
System.out.println(op.get());
}
收集
方法 | 描述 |
---|---|
collect(Collector c) | 将流转换为其他形式。接收一个Collector接口的实现,用于给Stream中元素做汇总的方法 |
Collector接口中方法的实现决定了如何对流执行收集操作(如收集到List、Set、Map)。但是Collectors实用类提供了很多静态方法,可以方便的创建常见收集器实例,具体方法如下表。
示例:
List<Emp> empList = Arrays.asList(
new Emp("张三", 18, 100.00,Status.FREE),
new Emp("李四", 36, 200.00,Status.BUSY),
new Emp("王五", 24, 500.00,Status.VOCATION),
new Emp("王五", 24, 600.00,Status.FREE),
new Emp("赵六", 14, 400.00,Status.BUSY)
);
@Test
public void t5() {
List<String> list1 = empList.stream()
.map(Emp::getName)
.collect(Collectors.toList());
System.out.println(list1);
HashSet<String> list2 = empList.stream()
.map(Emp::getName)
.collect(Collectors.toCollection(HashSet::new));
System.out.println(list2);
//平均值
Double avg = empList.stream()
.collect(Collectors.averagingDouble(Emp::getSalary));
//分组
Map<Status, List<Emp>> map = empList.stream()
.collect(Collectors.groupingBy(Emp::getStatus));
System.out.println(map.get(Status.BUSY));
//多级分组
Map<Status, Map<String, List<Emp>>> statusMapMap = empList.stream()
.collect(Collectors.groupingBy(Emp::getStatus, Collectors.groupingBy(
e -> {
if (e.getAge() <= 35) {
return "青年";
} else if (e.getAge() <= 50) {
return "中年";
}
return "老年";
}
)));
Map<String, List<Emp>> stringListMap = statusMapMap.get(Status.BUSY);
List<Emp> qn = stringListMap.get("青年");
List<Emp> zn = stringListMap.get("中年");
System.out.println("请年:" + qn);
System.out.println("中年:" + zn);
//分区
Map<Boolean, List<Emp>> partitionBySalary = empList.stream()
.collect(Collectors.partitioningBy(e -> e.getSalary() > 300));
System.out.println("条件为真的:" + partitionBySalary.get(true));;
System.out.println("条件为假的:" + partitionBySalary.get(false));;
//统计
DoubleSummaryStatistics doubleSummaryStatistics = empList.stream()
.collect(Collectors.summarizingDouble(Emp::getSalary));
System.out.println(doubleSummaryStatistics.getMax());
System.out.println(doubleSummaryStatistics.getCount());
System.out.println(doubleSummaryStatistics.getAverage());
//连接
String str = empList.stream()
.map(Emp::getName)
.collect(Collectors.joining(","));
System.out.println(str);
}
4、并行流与串行流
~这部分笔记会在后续补齐。
5、Optional类
Optional<T>类(java.util.Optional)是一个容器类,代表一个值存在或不存在,原来用null表示一个值不存在,现在Optional可以更好的表达这个概念。并且可以避免空指针异常。
常用方法:
方法 | 描述 |
---|---|
Optional.of(T t) | 创建一个Optional实例 |
Optional.empty() | 创建一个空的Optional实例 |
Optional.ofNullable(T t | 若t不为null,创建Optional实例,否则创建空实例 |
isParesent() | 判断是否包含值 |
orElse(T t) | 如果调用对象包含值,返回该值,否则返回t |
orElseGet(Supplier s) | 如果调用对象包含值,返回该值,否则返回s获取的值 |
map(Functaion f) | 如果有值对其处理,并返回处理后的Optional,否则返回Optional.empty() |
flatMap(Function mapper) | 与map类似,要求返回值必须是Optional |
示例:
@Test
public void t1() {
Emp emp = null;
Optional<Emp> op = Optional.of(emp);//该方法用于鉴别一个对象是否为null,若参数值为null,程序运行期间就会报空指针异常。便于快速定位空指针异常发生的对象。
System.out.println(op.get());
Optional<Emp> op2 = Optional.ofNullable(emp);
//Optional.ofNullable方法同上,但是它在使用null参数时会构建一个空的Optional实例
if(op2.isPresent()){//op对象中值不为null时返回true
System.out.println(op2.get());
}
Emp emp1 = op2.orElse(new Emp("张全", 18, 20.00, Status.BUSY));//调用对象中没有值时返回方法参数值
System.out.println(emp1);
Emp emp2 = op2.orElseGet(() -> new Emp("李立三", 18, 200.00, Status.BUSY));//如果调用对象没有值,返回s获取的值。
System.out.println(emp2);
Optional<String> str = op2.map(e -> e.getName());
System.out.println(str.get());
Optional<String> str2 = op2.flatMap(e -> Optional.of(e.getName()));
System.out.println(str2.get());
}
6、新时间 日期API
传统的时间格式化的线程安全问题
public class TestSimpleDateFormat {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//传统的解决时间格式化线程安全问题
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
Callable<Date> callable = new Callable<Date>() {
@Override
public Date call() throws Exception {
return DateFormatThreadLocal.convert("20161211");
}
};
ExecutorService pool = Executors.newFixedThreadPool(10);
List<Future<Date>> results = new ArrayList<Future<Date>>();
for (int i = 0; i < 10; i++) {
results.add(pool.submit(callable));
}
for (Future<Date> result : results) {
System.out.println(result.get());
}
pool.shutdown();
//Java8 以后的解决线程格式化时间类型的方案
DateTimeFormatter sdf = DateTimeFormatter.ofPattern("yyyyMMdd");
Callable<LocalDate> callable = new Callable<LocalDate>() {
@Override
public LocalDate call() throws Exception {
return LocalDate.parse("20161212",sdf);
}
};
ExecutorService pool = Executors.newFixedThreadPool(10);
List<Future<LocalDate>> results = new ArrayList<Future<LocalDate>>();
for (int i = 0; i < 10; i++) {
results.add(pool.submit(callable));
}
for (Future<LocalDate> result : results) {
System.out.println(result.get());
}
pool.shutdown();
}
}
public class DateFormatThreadLocal {
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>(){
protected DateFormat initialValue(){
return new SimpleDateFormat("yyyyMMdd");
}
};
public static Date convert(String source) throws ParseException {
return df.get().parse(source);
}
}
Java 8之后引入的时间类型 LocalDate、LocalTime、LocalDateTime
LocalDate、LocalTime、LocalDateTime类的实例是不可变的对象,分别表示ISO-8601日历系统的日期、时间、日期和时间。它们提供了简单的日期或时间,并不包含当前的时间信息,也不包含与时区相关的信息。
先来看看这三个对象的输出内容
LocalDateTime:2021-12-02T22:12:31.069
LocalDate:2021-12-02
LocalTime:22:12:31.069
LocalDate、LocalTime、LocalDateTime的构建方法 通过类型.now() 或类型.of(T…)方法获取时间对象的实例
LocalDateTime ldt = LocalDateTime.now();
LocalDate ld = LocalDate.now();
LocalTime lt = LocalTime.now();
LocalDateTime of = LocalDateTime.of(2000, 12, 12, 10, 30, 28);
LocalTime.of(T...)
...
新时间类的方法,以LoalDateTime为例。
LocalDateTime ldt = LocalDateTime.now();
LocalDateTime ldt3 = ldt.plusYears(2);//返回两年后的时间
LocalDateTime ldt4 = ldt.minusMonths(2);//返回2月前的时间
System.out.println(ldt.getYear());//获取当前时间的年
System.out.println(ldt.getDayOfMonth());//获取当前时间是该月的第几天
Instant :时间戳(以Unix元年:1970年1月1日00:00:00到某个时间之间的毫秒差)
Instant ins1 = Instant.now();//默认获取UTC时区
OffsetDateTime odf = ins1.atOffset(ZoneOffset.ofHours(8));//设置时间偏移量
System.out.println(ins1.toEpochMilli());//返回Unix元年到这个时间戳间隔的毫秒数
Instant ins2 = Instant.ofEpochSecond(60);//返回Unix元年60秒后的时间
Duration: 计算两个时间之间的间隔
Instant ins1 = Instant.now();
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
Instant ins2 = Instant.now();
Duration duration = Duration.between(ins1, ins2);
System.out.println(duration.toMillis());
System.out.println("-----------------------------------");
LocalTime lt1 = LocalTime.now();
try {
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
LocalTime lt2 = LocalTime.now();
System.out.println(Duration.between(lt1, lt2).toMillis());
Period: 计算两个日期之间的间隔
LocalDate ld1 = LocalDate.of(2015, 1, 1);
LocalDate ld2 = LocalDate.now();
Period period = Period.between(ld1, ld2);
System.out.println(period.getYears());
时间矫正器的应用
TemporalAdjuster:时间矫正器。有时我们可能需要获取例如:将日期调整到“下个周日”等操作。
TemporalAdjusters:提供了大量的实现了TemporalAdjuster的静态方法。
示例:
@Test
public void t1() {
LocalDateTime ldt = LocalDateTime.now();
ldt.withDayOfMonth(10);//将月份的时间给为10号
LocalDateTime ldt3 = ldt.with(TemporalAdjusters.next(DayOfWeek.SUNDAY));
System.out.println(ldt3);
//自定义:下一个工作日
LocalDateTime ldt5 = ldt.with(l -> {
LocalDateTime ldt4 = (LocalDateTime) l;
DayOfWeek dow = ldt4.getDayOfWeek();
if (dow.equals(DayOfWeek.FRIDAY)) {
return ldt4.plusDays(3);
} else if (dow.equals(DayOfWeek.SATURDAY)) {
return ldt4.plusDays(2);
} else {
return ldt4.plusDays(1);
}
});
System.out.println(ldt5);
}
时间格式化与时区的处理
DateTimeFormatter:格式化时间/日期
LocalDateTime ldt = LocalDateTime.now();
DateTimeFormatter dtf = DateTimeFormatter.ISO_DATE;
DateTimeFormatter dtf2 = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
System.out.println(ldt.format(dtf));
System.out.println(ldt.format(dtf2));
System.out.println(dtf2.format(ldt));
System.out.println(ldt.parse("2021年12月02日 21:46:54", dtf2));
ZonedDate、ZonedTime、ZonedDateTime
LocalDateTime ldt = LocalDateTime.now(ZoneId.of("Europe/Tallinn"));
System.out.println(ldt);
LocalDateTime ldt2 = LocalDateTime.now(ZoneId.of("Asia/Shanghai"));
ZonedDateTime zdt = ldt2.atZone(ZoneId.of("Asia/Shanghai"));
System.out.println(zdt);