概述
java8包含了很多新特性,这里我们简述几个常用的。
- Lambda表达式
- 方法引用
- Stream API
- 日期时间类
- Optional类
- 接口默认方法
- JavaScript引擎
1.Lambda表达式
官方解释:Lambda 表达式,是一个匿名函数,即没有函数名的函数。
1.1Lambda的用处
其实我更愿意把它理解为函数式接口的实现。
函数式接口(@FuctionalInterface):只有一个方法的接口
1.1.1 简单示例
举个例子,假如我们现在有一个接口:
@FunctionalInterface
public interface Arithmetic {
int add(int a, int b);
}
如果我们想使用这个接口,那么需要先定义一个实现类,再创建实现类的实例。代码如下:
实现类:
public class ArithmeticImpl implements Arithmetic{
@Override
public int add(int a, int b) {
return a + b;
}
}
具体调用:
public class MainDemo {
@Test
public void test(){
Arithmetic arithmetic = new ArithmeticImpl();
int result = arithmetic.add(1, 2);
System.out.println(result);
}
}
但如果使用lambda表达式的话,则可以直接免去定义实现类这步,直接使用lambda表达式作为接口实现。代码如下:
public class MainDemo {
@Test
public void test2(){
Arithmetic arithmetic = (a, b) -> a + b;
int result = arithmetic.add(1, 2);
System.out.println(result);
}
}
1.1.2 再来个Lambda范例
如果上面没看出来差别,我们再举个更常见的例子:创建线程。
方式一:定义Runnable实现类
1.定义Runnable实现类
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("MyRunnable运行啦");
}
}
2.创建Thread并运行
public class ThreadTest {
@Test
public void test1(){
Runnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
}
方式二:匿名实现类
直接创建匿名实现类。比起上面省去了定义实现类这步。
public class ThreadTest {
@Test
public void test2(){
Runnable runnableImpl = new Runnable() {
@Override
public void run() {
System.out.println("匿名实现类 运行啦");
}
};
Thread thread = new Thread(runnableImpl);
thread.start();
}
}
方式三:lambda表达式
那针对方式二,是不是可以更简单呢。
答案当然是可以啦,因为Runnable是一个函数式接口,只有一个方法嘛,所以我们可以连这个方法名都省去,只要有方法体就行。
所以我们可以直接用lambda表达式,作为它的实现类。
public class ThreadTest {
@Test
public void test3(){
Runnable lambda = () -> System.out.println("lambda实现类 运行啦");
Thread thread = new Thread(lambda);
thread.start();
}
}
通过上面两个例子,我们可以了解到lambda表达式可使用的地方。
即:凡是需要函数式接口实现类的地方,都可以用lamba表达式作为实现类。
(除此之外,可能还有其他地方可用lamba表达式,但恕我知识有限,暂时还不太清楚)
1.2 lambda表达式结构体
知道了它的用处,也更要知道它的写法。lambda既然是匿名类函数,那肯定也是有方法声明和方法体,结构如下:
(...) -> {...}
主要分为三部分:
1. () 参数部分
声明参数列表。接口中已声明参数类型个数,此处无需声明参数类型,仅需标明参数名。
2. -> 箭头符号
区分参数部分和方法体部分。
3. {} 方法体部分
就是正常方法体,和普通方法体一样写就行。
需要特殊说明的地方简单介绍下,举几个例子大家就明白了
参数部分说明:
1.如果参数列表为空的话,参数部分为空即可
() -> {System.out.println("hello")}
2.参数列表只有一个的时候,括号()可省略
(a) -> {System.out.println("hello " + a)}
a -> {System.out.println("hello " + a)}
3.多个参数的话,挨个声明变量名即可
(a,b,c) -> {
System.out.println("hello " + a);
System.out.println("hello " + b);
System.out.println("hello " + c);
}
箭头部分:
箭头部分固定格式,不可缺少,不可改动。
方法体部分:
1.正常结构:
(a) -> {
System.out.println("hello " + c);
}
2.如果方法体中只有一行代码,{}括号可以省略
a -> System.out.println("hello " + a);
3.如果方法有返参,正常使用return关键字即可
(a,b,c) -> {
System.out.println("hello " + a);
System.out.println("hello " + b);
System.out.println("hello " + c);
return a + b + c;
}
4.如果方法只有一行且有反参,{} 可省略,return 关键字可省略。
(a,b,c) -> a + b + c;
1.3 java.util.function包
JDK 1.8 API包含了很多内建的函数式接口,简单枚举如下:
name | type | description |
---|---|---|
Consumer | Consumer< T > | 接收T对象,不返回值 |
Predicate | Predicate< T > | 接收T对象并返回boolean |
Function | Function< T, R > | 接收T对象,返回R对象 |
Supplier | Supplier< T > | 提供T对象(例如工厂),不接收值 |
UnaryOperator | UnaryOperator | 接收T对象,返回T对象 |
BinaryOperator | BinaryOperator | 接收两个T对象,返回T对象 |
好多时候需要使用上述接口的一些实现类,那这些地方就可以直接使用lamba表达式啦。
比如我们后面要说的Stream类,就大量依赖这些接口,所有我们才经常说Stream要配合lambda表达式一起使用。
public interface Stream<T> extends BaseStream<T, Stream<T>> {
Stream<T> filter(Predicate<? super T> predicate);
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
IntStream mapToInt(ToIntFunction<? super T> mapper);
LongStream mapToLong(ToLongFunction<? super T> mapper);
...
}
2.方法引用
方法引用通过方法的名字来指向一个方法,可以使语言的构造更紧凑简洁,减少冗余代码。
方法引用使用一对冒号 :: 表示调用某个方法,通常格式为下面两种
ClassName::MethodName
ObjectName::MethodName
直接讲定义不太好理解,我们直接看个demo。还是上面那个Arithmetic接口。
@FunctionalInterface
public interface Arithmetic {
int add(int a, int b);
}
我们用lamba表达式来实现这接口
public class ReferDemo {
@Test
public void test1(){
Arithmetic arithmetic = (a, b) -> a + b;
System.out.println(arithmetic.add(1,2));
}
}
上面代码已经很简便了,但还可以再简便一些吗?当然可以, a+b 这个方法体可以直接用Integer.sum()接口替换。替换后结果代码如下:
Arithmetic arithmetic = (a, b) -> Integer.sum(a,b);
看到这种格式,就到了使用方法引用的时候了,直接使用方法引用再次替换上述代码:
public class ReferDemo {
@Test
public void test2(){
Arithmetic arithmetic = Integer::sum;
System.out.println(arithmetic.add(1,2));
}
}
看到这里我们可以总结下方法引用是什么了:
如果lambda表达式中,仅是调用了某个类或对象的方法,且表达式的入参与方法的入参一致,那这个lambda表达式就可以直接简写为方法引用。
3.Stream API
终于到了Java8 的重头戏,Stream类是我们日常工作中处理数据集合类最常用的工具类。
Stream API的特点是:
- Stream API提供了一套新的流式处理的抽象序列;
- Stream API支持函数式编程和链式操作;
- Stream可以表示无限序列,并且大多数情况下是惰性求值的。
(说实话上述概念我不懂,我只会用...)
3.1使用简述
使用Stream处理集合使用时具体分为三个步骤:
一个流式过程中,可以包含多个处理操作,以及一个终结操作。
1.生成流
toStream() 生成流
toParallel() 生成并行流 (数据量不是很大的话,转换流的成本会比并行节省的时间成本高)
2.处理
处理过滤stream中的数据,但不生成最终结果。
filter() 过滤某些数据,将符合条件的元素提取到新的流中的操作
map() 将每个元素转换成其他对象,生成新的流
sort() 将元素按规则排序,生成新的流
...
3.终结/汇总
针对前面过程中处理过的数据进行呢汇总,生成最终结果。
forEach() 遍历每一个元素进行操作
collect() 汇总数据,转成集合
max() 获取最大值
count() 获取数量
anyMatch() 是否存在元素满足条件
...
3.3 常见使用
我们下面的案例都会使用个Person类,这类事先定义好。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person {
private String name;
private int age;
}
1.遍历
void forEach(Consumer<? super T> action);
先生成流,再对每一个元素进行打印输出。
注意看哦,我们这里还有了上面说过的方法引用
public class StreamDemo {
List<Person> personList;
@Before
public void init(){
personList = new ArrayList<>();
personList.add(new Person("xiang",22));
personList.add(new Person("ming",15));
}
@Test
public void test1(){
personList.stream().forEach(System.out::println);
}
}
2.过滤
Stream filter(Predicate<? super T> predicate);
只有满足条件的数据才会传递下去生成新的流。即年龄大于18的人员才会被打印出来。
public class StreamDemo {
List<Person> personList;
@Before
public void init() {
personList = new ArrayList<>();
personList.add(new Person("xiang", 22));
personList.add(new Person("ming", 15));
}
@Test
public void test2() {
//过滤,获取年龄大于18的人员
personList.stream()
.filter(s -> s.getAge() > 18)
.forEach(System.out::println);
}
}
3.转换
Stream map(Function<? super T, ? extends R> mapper);
将流中的元素转换为其他类型的元素。
这里将Person类型的对象转换为String类型的名字了。
public class StreamDemo {
List<Person> personList;
@Before
public void init() {
personList = new ArrayList<>();
personList.add(new Person("xiang", 22));
personList.add(new Person("ming", 15));
}
@Test
public void test3() {
//转换,Person 转为 String
personList.stream()
.map(s->s.getName() + "," + s.getAge())
.forEach(System.out::println);
}
}
4.排序 + 转换
既然是流式处理,那么在写法上,其实也是可以流式传递的。
下面就是一个先排序,再转换的一个案例。
public class StreamDemo {
List<Person> personList;
@Before
public void init() {
personList = new ArrayList<>();
personList.add(new Person("xiang", 22));
personList.add(new Person("ming", 15));
}
@Test
public void test4() {
//过滤,获取年龄大于18的人员
personList.stream()
.sorted(Comparator.comparing(Person::getAge))
.map(s->s.getName() + "," + s.getAge())
.forEach(System.out::println);
}
}
5.转为List
此处显示了一个将Person转换为String,最终收集为一个List的操作。
public class StreamDemo {
List<Person> personList;
@Before
public void init() {
personList = new ArrayList<>();
personList.add(new Person("xiang", 22));
personList.add(new Person("ming", 15));
}
@Test
public void test5() {
List<String> descList = personList.stream()
.map(s -> s.getName() + "," + s.getAge())
.collect(Collectors.toList());
System.out.println(descList);
}
}
6.转为Map
这里将一个list转为了一个map:
name作为key,Person对象作为value。
public class StreamDemo {
List<Person> personList;
@Before
public void init() {
personList = new ArrayList<>();
personList.add(new Person("xiang", 22));
personList.add(new Person("ming", 15));
personList.add(new Person("xiao", 15));
}
@Test
public void test6() {
Map<String, Object> objectMap = personList.stream().collect(Collectors.toMap(Person::getName, s -> s));
System.out.println(objectMap);
}
}
7.按年龄分组
这里将list的元素按年龄分组:
age作为key,List作为value
public class StreamDemo {
List<Person> personList;
@Before
public void init() {
personList = new ArrayList<>();
personList.add(new Person("xiang", 22));
personList.add(new Person("ming", 15));
personList.add(new Person("xiao", 15));
}
@Test
public void test7() {
Map<Integer, List<Person>> group = personList.stream().collect(Collectors.groupingBy(Person::getAge));
System.out.println(group);
}
}
8.获取年龄总和
public class StreamDemo {
List<Person> personList;
@Before
public void init() {
personList = new ArrayList<>();
personList.add(new Person("xiang", 22));
personList.add(new Person("ming", 15));
personList.add(new Person("xiao", 15));
}
@Test
public void test8() {
int ageSum = personList.stream().mapToInt(Person::getAge).sum();
System.out.println(ageSum);
}
}
诸如此类的操作,后续大家挨个试下就会觉得Stream超级好用了。
4.日期时间类
因为之前的Date时间类,无论是计算还是格式化,都不是很好用。所以java8中推出了java.time包。其中的LocalDate、LocalDateTime等时间类无论是效率还是使用上都以前提升很多。
4.1 日期、时间的加减
public class TimeTest {
@Test
public void test1(){
LocalDate today = LocalDate.now();
LocalDate yesterday = today.minusDays(1);
LocalDate lastMonthDay = today.minusMonths(1);
System.out.println(today);
System.out.println(yesterday);
System.out.println(lastMonthDay);
}
@Test
public void test2(){
LocalDateTime now = LocalDateTime.now();
LocalDateTime tomorrow = now.plusDays(1);
LocalDateTime nextMonthDay = now.plusMonths(1);
System.out.println(now);
System.out.println(tomorrow);
System.out.println(nextMonthDay);
}
}
输出:
4.2 日期时间的格式化
public class TimeTest {
@Test
public void test3(){
//定义格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
//字符串转日期
LocalDate localDate = LocalDate.parse("2021-01-01", formatter);
System.out.println(localDate);
//日期转字符串
LocalDate today = LocalDate.now();
String todayStr = today.format(formatter);
System.out.println(todayStr);
}
}
5.Optional类
在java8之前,我们针对某个对象可能要做一些判空处理,而Optional类则是帮我减少这类判空处理的代码逻辑。
它提供了一些诸如isPresent() 、orElse()等好用的方法。
Optional在使用时,可依据具体场景使用。
public class OptionalDemo {
List<Person> personList;
@Before
public void init() {
personList = new ArrayList<>();
personList.add(new Person("ming", 15));
personList.add(new Person("xiao", 15));
}
@Test
public void test1(){
Optional<Person> first = personList.stream().filter(s->"xiang".equals(s.getName())).findFirst();
//如果first.get()为空的话,提供一个默认值对象
Person xiang = first.orElse(new Person("xiang",25));
System.out.println(xiang);
}
}
6.接口默认方法
6.1 接口默认实现
接口可以有默认实现了,实现类可以直接使用它的默认实现。
接口:
public interface Arithmetic {
default int add(int a, int b){
return a +b;
}
}
实现类:
public class ArithmeticImpl implements Arithmetic{
}
Test:
public class InterfaceTest {
@Test
public void test1(){
Arithmetic arithmetic = new ArithmeticImpl();
System.out.println(arithmetic.add(1,2));
}
}
输出:
6.2接口静态方法
接口还可以拥有静态方法了,外部调用时就像调用类的静态方法一样使用
接口:
public interface Arithmetic {
static String hello(String name){
return "hello " + name;
}
}
Test:
public class InterfaceTest {
@Test
public void test2(){
System.out.println(Arithmetic.hello("xiang"));
}
}
7.JavaScript引擎
java8提供了一个能够解析js脚本的引擎,可以运行js代码
public class JSDemo {
/**
* var fun1 = function(name) {
* print('Hello ' + name);
* return "Hi!";
* }
*
* var fun2 = function (object) {
* print(Object.prototype.toString.call(object));
* };
*/
@Test
public void test1() throws ScriptException, NoSuchMethodException {
//js脚本
String script ="var fun1 = function(name) {\n" +
" print('Hello ' + name);\n" +
" return \"Hi!\";\n" +
"}\n" +
"\n" +
"var fun2 = function (object) {\n" +
" print(Object.prototype.toString.call(object));\n" +
"};";
//声明js引擎
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
//处理js脚本
engine.eval(script);
//调用js方法
Invocable invocable = (Invocable) engine;
Object result = invocable.invokeFunction("fun1", "Nashorn");
System.out.println(result);
System.out.println(result.getClass());
}
}
输出结果:
8.写在最后
无论怎么简便,不过都是语法糖而已,编译后的字节码还是没什么变化。
但只要能简化美化我们代码的语法,都应该去尝试写写。
毕竟代码是要给人看的,代码优雅些,更利于团队和谐嘛!