《Java实战》学习记录

本文回顾了Java相关知识,介绍了行为参数化和Lambda表达式,可使代码更灵活;阐述了Stream流,能替代循环迭代;讲解了Collectors收集器用于高级规约。还提及函数式编程、重构测试调试方法、Java DSL、并行并发、新API、默认方法等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


前言

花了接近一个月的时间过了一遍这本书,现在也该回顾一下都看了哪些东西了,这里更多是写自己的记录,没有把书上的内容都记下来。


一、行为参数化和Lambda表达式

在过去,我们习惯用调用方法的方式,实现不同方法间的数据传输,一个个方法间关联度较大,随着开发的代码越来越多,假如需求发生了变更,需要改写原来的方法,这是非常繁琐的,也容易出问题。随着Java的不断发展,我们可以使用行为参数化的方法,使代码更加灵活,以应对需求的变更。

1.行为参数化

先来看个例子,筛选颜色是红的苹果,最直白的做法是:

for(Apple a : apples){
	if(a.getColor().equals(RED)){}
}

然后,写成一个方法

public List<Apple> getRedApple(List<Apple> apples){
	new ArrayList<>()
	for(){
		if(a.getColor().equals(RED)){
			// 加入List
		}
	}
	return 
}

你可以再细分一下,写个判断的方法:

public boolean predicate(Apple a){
	return a.getColor().equals(RED);
}

然后调用:

if(predicate(a)){}

但是现在,我有了新的需求,我要颜色是绿色的苹果,还有重量、品种等待筛选条件,我还要对结果进行排序等等操作。相信这些需求写起来是很简单的,但也很无聊,还有点枯燥,最后写出来一大堆代码,看起来很臃肿,不过,我们有接口这个好用的东西。

public interface ApplePredicate{
	boolean test(Apple apple);
}

以前了解接口的时候,只知道它能用来建模,比如弄个animal接口,然后什么动物类都去实现它,里面需要实现一些通用的方法,也可以添加和重写。当然对方法建模也是可以的:

public class AppleWeightPredicate implements ApplePredicate{
	public boolean test(Apple apple){
		return apple.getWeight() < 150;
	}
}

public class AppleRedPredicate implements ApplePredicate{
	public boolean test(Apple apple){
		return apple.getColor().equals(RED);
	}
}

接下来,我们用接口来实现行为参数化:

public List<Apple> filterApples(List<Apple> apples, ApplePredicate p){
	new ArrayList<>()
	for(Apple a : apples){
		if(p.test(a)){
		}
	}
	return 
}

可以看到,我们用一个接口对象作为参数,向方法传入一个方法,p.test(a)就是在调用方法返回一个boolean
然后,将之前写的策略类传入:

List<Apple> res = filterApples(apples, new AppleWeightPredicate())

这样就能实现AppleWeightPredicate里面方法的调用了
虽然但是,这样写的话,需要提前声明方法类,显然工作量还是不小,每个方法都要声明一下,于是我们有匿名类:

List<Apple> res = filterApples(apples, new ApplePredicate() {
	public boolean test(Apple apple){
		return apple.getWeight() < 150;
	});

这样写就不用提前声明了,可是,看起来还是不好,很笨重。
Java8给了个很好用的东西——Lambda表达式

List<Apple> res = filterApples(apples, (Apple a) -> a.getWeight() < 150);

这实在是太酷啦!

2.Lambda表达式

Lambda表达式其实就是个简洁的可传递的匿名函数,不用声明名称,但是又参数列表,有函数体,可以当作参数传递给接口。
来看看Lambda表达式的格式:
(参数) -> 表达式
(参数) -> {代码块;}
举两个错误的例子就知道了:

// 错误的写法
() -> return 1; 
() -> {1;}
// 正确的写法:
() -> {retern 1;}
() -> 1

3.函数式接口

为了更好地理解,我们用函数描述符来描述一个Lambda表达式,当然描述函数也是可以的
(Apple a, Apple b) -> a.getWeight()
它的函数描述符是:
(Apple, Apple) -> int
用这个是为了更好地匹配函数式接口
函数式接口就是我们前面讲的用来传递方法的接口
函数式接口只定义了一个抽象方法

public interface Runnalbe { // () -> void
	void run();
}

public interface Comparator<T> { // (T,T) -> int
	int compare(T o1, T o2);
}

当我们向函数式接口传递Lambda表达式时,要注意Lambda表达式的函数描述符和接口的函数描述符是否匹配

Runnable run = () -> System.out.println("1") ;
函数描述符都是 () -> void , 正确

在Java中,有函数式接口的注释@FunctionalInterface

这里有一些常见的函数式接口:

函数式接口函数描述符
Predicate < T >T -> boolean
Consumer< T >T -> void
Function< T,R >T -> R
Supplier< T >() -> T
UnaryOperator< T >T -> T
BinaryOperator< T >(T,T) -> T
BiPredicate< T, U >(T,U) -> boolean
BiConsumer< T, U>(T, U) -> void
BiFunctione< T, U>(T,U) -> R
Comparator< T >(T,T) -> int

注:还有很多其他的,可以自己探索
注:一些接口有基本类型的特化,比如Predicate有IntPredicate,Function有IntToDoubleFunction,ToIntFunction<T>等等,可以自己探索

4.特殊规则

(1)类型推断

 /**
  * 类型推断 编译器可根据上下文推断出lambda的参数类型 可能易读,也可能不易读
  */
 Predicate<String> p1 = (s) -> list.add(s); // 省略了String

(2)void兼容规则

 /**
  *  特殊情况 list.add 返回一个 boolean ,但是 list.add 是一个表达式,所以可用于 () -> void
  */
 List<String> list = new ArrayList<>();
 Predicate<String> p = (String s) -> list.add(s);
 Consumer<String> b = (String s) -> list.add(s);
 Consumer<String> b2 = (String s) -> s.isEmpty();
 Consumer<String> b3 = (String s) -> {s.isEmpty();};

(3)变量限制

 /**
  *  变量限制 变量必须是final形式,如果a被赋值两次,则无法编译通过
  */
 int a = 1;
// a = 2;  错误
 Runnable r = () -> System.out.println(a);
// a = 2;  错误

5.方法引用

为了更方便阅读,有时候可以用方法引用替代Lambda表达式

 // 一个例子
 apples.sort(comparing(Apple::getWeight);
 // 静态方法的方法
 ToIntFunction<String> stringToIntFunction = (String s) -> Integer.parseInt(s);
 ToIntFunction<String> stringToIntFunction1 = Integer::parseInt;  // 方法引用
 // 类型实例方法的方法
 BiPredicate<List<String>, String> contains = (List, element) -> List.contains(element);  //这里用了类型推断
 BiPredicate<List<String>, String> contains1 = List<String>::contains; // 方法引用
 // 构造函数的方法引用
 Supplier<Apple> apple = () -> new Apple();
 Apple a1 = apple.get();
 Supplier<Apple> apple1 = Apple::new;
 Apple a2 = apple1.get();
 Function<Integer, Apple> apple2 = Apple::new;
 Apple a3 = apple2.apply(1);
 BiFunction<Color, Integer, Apple> apple3 = Apple::new;
 Apple a4 = apple3.apply(Color.BLUE, 1);

6.复合

方法描述
reversed逆序
thenComparing比较器链
negate
and
or
andThen函数复合
compose跟andThen反过来
// 逆序
apples.sort(comparing(Apple::getWeight).reversed());
// 比较器链
apples.sort(comparing(Apple::getWeight).reversed().thenComparing(Apple::getColor));
// 非
Predicate<Apple> notRedApple = (Apple a) -> a.getColor().equals(RED).negate();
// and
Predicate<Apple> RedAndHeavyApple = (Apple a) -> a.getColor().equals(RED)
									.and(a -> a.getWeight() > 150);
// or 跟and用法一样
// 函数复合
Function<Integer, Integer> f = x -> x + 1;
Function<Integer, Integer> g = x -> x * 3;

Function<Integer, Integer>m = f.andThen(g);
Function<Integer, Integer>n = f.compose(g);

m.apply(1); // 结果为 (1 + 1) * 3
n.apply(1); // 结果为 1 * 3 + 1

//用andThen构造流水线
Function<String,String> addHeader = Letter::addHeader;
Function<String,String> transformationPipeline
        = addHeader.andThen(Letter::checkSpelling)
                   .andThen(Letter::addFooter);

7.小结

用一个例子总结这一章的内容:

 /**
  *  List.sort(Comparator<? super E > c) 这是一个void函数,参数是一个Comparator对象
  */
 List<Apple> inventory = new ArrayList<>();
 // 使用创建一个针对Apple的比较类方式,继承接口
 class AppleComparator implements Comparator<Apple>{
     public int compare(Apple a1, Apple a2){
         return a1.getWeight().compareTo(a2.getWeight());
     }
 }

 inventory.sort(new AppleComparator());

 // 匿名类方式
 inventory.sort(new Comparator<Apple>(){
     public int compare(Apple a1, Apple a2){
         return a1.getWeight().compareTo(a2.getWeight());
     }
 });

 // lambda表达式方式
 inventory.sort((Apple apple_1, Apple apple_2) -> apple_1.getWeight().compareTo(apple_2.getWeight()));
 // 紧凑一点
 // 使用Comparator.comparing接口
 inventory.sort(comparing( apple_3 -> apple_3.getWeight()));

 // 方法引用
 inventory.sort(comparing(Apple::getWeight));


二、Stream流

Java8提供了一个十分方便的工具Stream流来替代过去使用循环迭代遍历数据的方式,既提高了代码的可读性,又方便编写,还提高了性能,避免了共享变量的数据冲突问题,总之,用就完事了。

1.了解流

从一个例子了解流

List<Dish> menu = ...           //一个菜单
List<String> nameList = menu    //获取菜名列表
					  .stream() 
					  .filter(Dish::isMeat) // 筛选出肉食
					  .map(Dish::getName)   // 转换成名字
					  .limit(3)             // 选取前3个
					  .collect(toList());   // 将流转化成Lsit

流就是一个对数据源进行处理的工具,用声明性方式更加易读

集合
遍历能重复使用只能遍历一次
计算计算完需放入集合中只需计算需要的部分
迭代外部迭代内部迭代

2.流操作

流的常规操作如下:
(1)中间操作

操作返回类型操作参数函数描述符描述
filterStream< T >Predicate< T>T -> boolean筛选
mapStream< R >Function< T,R >T -> R映射
flatMapStream< R >Function< T,Stream< R > >T -> Stream< R >扁平化
limitStream< T >long截短
skipStream< T >long跳过
sortedStream< T >Comparator< T >(T,T) -> int排序
distinctStream< T >去重

(2)终端操作

操作返回类型操作参数函数描述符描述
forEachvoidConsumer< T >T -> void消费所有元素
countlong返回流的元素个数
collectRCollector<T, A, R>将流规约成一个集合
anyMatchbooleanPredicate< T>T -> boolean是否有一个满足
allMatchbooleanPredicate< T>T -> boolean是否所有都满足
noneMatchbooleanPredicate< T>T -> boolean是否都不满足
findAnyOptional< T >返回一个Optional,包含随机一个元素
findFirstOptional< T >返回一个Optional,包含第一个元素
reduceOptional< T >BinaryOperator(T,T) -> T规约

3.流的扁平化

以一个例子解释流的扁平化

List<String> words = Arrays.asList("Yes", "No");
// 获取所有字母
List<String> uniqueCharacters = words.stream() // Stream<String>
                .map(word -> word.split(""))   // Stream<String[]>
                .flatMap(Arrays::stream)       // Stream<String>
                .distinct()
                .collect(Collectors.toList());

当我们将words中的字符串都拆分成字符时,把每个字符串拆成了一个数组,也就是

["Yes","No"]   //  这里用[]表示流
-map(word -> word.split("")) ->  [{"Y","e","s"},{"N","o"}] 
-flatMap(Arrays::stream)     ->  ["Y","e","s","N","o"]

当我们使用flatMap时,我们将数组类每个元素都转化成了流的元素

再来看一个例子:

 /**
 *  给两个列表, 得到所有数对
 *  (1,3) (1,4) (2,3) (2,4) (3,3) (3,4)
 */
List<Integer> num1 = Arrays.asList(1,2,3);
List<Integer> num2 = Arrays.asList(3,4);
// 这里数对用数组表示
List<int []> paris =
        num1.stream() // Stream<Integer>
        .flatMap(i -> num2.stream() //如果使用map,得到的是Stream<Stream<int>>
                        .map(j -> new int[]{i,j})) // Stream<int[]>
        .collect(Collectors.toList());
/**
 * 扩展 返回和可被3整除的数对
 */
List<int []> paris_3
        = num1.stream()
        .flatMap(i -> num2.stream()
                .filter(j -> (j + i)%3 == 0)
                .map(j -> new int[]{i,j}))
        .collect(Collectors.toList());

4.流的归约

// 求和
int sum = numbers.stream().reduce(0, (a,b) -> a+b);//初始值,操作
// 求积
int pro = numbers.stream().reduce(1,(a,b) -> a*b);
// 最值
Optional<Integer> min = numbers.stream().reduce(Integer::min);  //注:Optional后面会说

注:不止这些功能

5.数值流

方法描述
rangeClosed/range生成范围数值流
mapToInt流转数值流
boxed数值流转流
max/min最值
sum求和
//范围数值
IntStream numbers = IntStream.rangeClosed(1,100) // 1-100,如果用range(1,100)是1-99
							 .filter(n -> n % 2 == 0); //筛选偶数

// 如果我们想计算总值
int res1 = transactions.stream()
        .map(Transaction::getValue)   //转化为Stream<Integer>
        .reduce(0,Integer::sum);      //规约
// 使用数值流求和
int res2 = transactions.stream()
        .mapToInt(Transaction::getValue)  // 将流转化为数值流IntStream
        .sum();  // 直接使用IntStram().sum()
// 把数值流转为stream
Stream<Integer> stream = transactions.stream().mapToInt(Transaction::getValue)
        .boxed();
//最值
OptionalInt Max = transactions.stream()
        .mapToInt(Transaction::getValue)  
        .max();         // 返回一个OptionalInt
int max = Max.orElse(1) //Optional的用法,如果为空返回1

一个难一点的例子,生成勾股数流

Stream<double[]> pythagoreanTriples =
    IntStream.rangeClosed(1,100).boxed() //IntStream 转 Stream
        .flatMap(a ->  // flatmap : 根据a的值,创建三元数流,将a映射到三元流的话,会得到一个流构成的流,flatmap将三元数流扁平化为一个流
                IntStream.rangeClosed(a,100) // b为 (a,100)
                        .mapToObj(b ->
                                new double[]{a,b, Math.sqrt(a*a + b*b)})
                        .filter(t -> t[2] % 1 == 0)
                );

6.构建流

方法描述
iterate迭代
generate生成
Stream.of构建流
Arrays.stream用数组构建流
// 字符串
Stream<String> stringStream = Stream.of("a","b","c");
// 数组
int[] numbers1 = {1,2,3,4,5};
int sum1 = Arrays.stream(numbers1).sum();
// 文件
try(
    Stream<String> lines = Files.lines(Paths.get("src/main/java/file/data.txt"), Charset.defaultCharset())){
}
catch(IOException e){
}
// 无限流
1.迭代
//返回前一个数+2
Stream.iterate(0, n -> n + 2)
       .limit(10)
       .forEach(System.out::println);
// 斐波拉契数列
// 0,1,1,2,3,5  后一个数是前两个数之和
// 元组序列
// (0,1) (1,1,) (1,2) (2,3)
Stream.iterate(new int[]{0,1},n -> new int[]{n[0]+n[1], n[0]+n[1]+n[1]})
       .limit(20)
       .forEach(n -> System.out.println(n[0] +", "+ n[1]));

2.生成
Stream.generate(Math::random)
       .limit(5)
       .forEach(System.out::println);

注:上述方法只是从书上记录的,肯定不全,实际使用还是要多探索探索


三、Collectors收集器

在之前我们学了用reduce来进行规约,Java8提供了Collector接口来进行更高级的规约操作

1.归约和汇总

方法描述
counting计数
maxBy/minBy最值,返回一个Optional
summingInt求和
averagingInt平均值
summarizingInt打印,类似toString,包含最大值平均值等
joinning把所有元素连接成字符串
reducing广义归约,使用BinaryOperator将元素逐个结合
List<Dish> menu = ...
// 计数,有多少道菜
long howManyDishes = menu.stream().collect(counting());
// 最值 maxBy
Optional<Dish> mostCalorieDish = menu.stream().collect(maxBy(comparing(Dish::getCalories)));
// 取和 summing
int totalCalorie = menu.stream().collect(summingInt(Dish::getCalories));
// 平均值 averaging
double aveCalorie = menu.stream().collect(averagingDouble(Dish::getCalories));
// IntSummaryStatistics 获取状态
IntSummaryStatistics menuStatics = menu.stream().collect(summarizingInt(Dish::getCalories));
// joinning
String names = menu.stream().map(Dish::getName).collect(joinning(", "));

// reduce 更加灵活地归约汇总                   初始值   转换函数         累积函数
totalCalorie = menu.stream().collect(reducing(0, Dish::getCalories, Integer::sum)); // collector的reduce
totalCalorie = menu.stream().map(Dish::getCalories).reduce(0, Integer::sum); // 流的reduce
totalCalorie = menu.stream().map(Dish::getCalories).reduce(Integer::sum).get(); // 流的reduce返回的是一个Optional,get成功的前提是Optional值不为空
totalCalorie = menu.stream().map(Dish::getCalories).collect(reducing(Integer::sum)).get(); // reduce 是 T,T -> T 接收与返回类型需要相同
totalCalorie = menu.stream().mapToInt(Dish::getCalories).sum(); // 用数值流 更加简洁

2.分组

方法描述
groupingBy分组,返回一个map,分组条件为键,元素为值
mapping映射
collectingAndThen用另一个收集器处理结果
partitioningBy分区,就是分成两个,true和false
Map<Boolean, List<Dish>> dishesByType = menu.stream().collect(groupingBy(Dish::isVegetarian));
Map<String, List<Dish>> dishesByHigh = menu.stream().collect(groupingBy(d -> {
    if (d.getCalories() > 400) return ">400";
    else return "<=400";
}));

还可以多级分组,也就是嵌套使用groupingBy

Map<Boolean, Map<String, List<Dish>>> dishesByTypeAndHigh = menu.stream()
        .collect(groupingBy(Dish::isVegetarian, 
        		 groupingBy(d -> {
                   	 if (d.getCalories() > 400) return ">400";
                     else return "<=400";
                 })));

当然,还可以进行其他操作,写在groupingngBy的第二个参数中

// 每类的数量
Map<Boolean, Long> typeCount = menu.stream()
        .collect(groupingBy(Dish::isVegetarian,
                counting()));
// 每类热量最高的
Map<Boolean, Optional<Dish>> maxCaDishOp = menu.stream()
        .collect(groupingBy(Dish::isVegetarian,
                maxBy(comparing(Dish::getCalories))));
// 使用mapping映射结果
Map<Boolean, List<String>> dishNames = menu.stream()
		.collect(groupingBy(Dish::isVegetarian,
							mapping(Dish::getName, toList())));

Map<Boolean, Set<String>> groupMapTest = menu.stream().collect(groupingBy(Dish::isVegetarian,
        mapping(dish -> {
            if (dish.getCalories() > 400) return "high";
            else return "low";
        }, toCollection(HashSet::new))));

Map<Boolean, String> groupMapTest2 = menu.stream().collect(groupingBy(Dish::isVegetarian,
        mapping(dish -> {
            if (dish.getCalories() > 400) return "high";
            else return "low";
        }, joining())));
        
// 把收集器结果转化为另一种类型
Map<Boolean, Dish> maxCaDish = menu.stream()
        .collect(groupingBy(Dish::isVegetarian,               // 收集器
                collectingAndThen(                            // 收集器
                        maxBy(comparing(Dish::getCalories)),  // 收集器
                        Optional::get)));                     // 转换函数

3.其他

方法描述
toList转化为一个List
toSet转化为一个Set,去除重复项
toCollection转化到新建的集合中,需要一个(集合类型::new)参数

注:你还可以根据Collector接口自定义一个Collector收集器,以便使用,这里就不写了


四、Collection API的增强功能

1.List

方法描述
removeIf移除指定元素,不用循环遍历
replaceAll用一个元素替换所有满足条件的元素
forEach遍历

注:这些方法的详细信息可以去源码看

animals.removeIf(animal -> animal.length() > 3);
animals.replaceAll(animal -> animal.toUpperCase()); //变成大写

// 其他一些,用上Lambda表达式和方法引用
animals.sort(Comparator.comparing(animal->animal.length())); // 排序
animals.forEach(System.out::println); // forEach 遍历

2.Set

新增removeIf

3.Map

方法描述
forEach遍历
forEachOrdered保证顺序遍历,在并行下可保证顺序
Entry.comparingByValue/Entry.comparingByKey排序
getOrDefault根据键找值,若键不存在则返回默认值
computeIfAbsent若没有Key或有Key无值,则计算值
computeIfPresent若有Key,则计算值,修改
compute指定Key计算值存储到Map
remove删除
removeIf根据条件删除
replace若键存在则替换
merge合并
Map<String, Integer> students = new HashMap<>();
students.put("张三",11);
// 对map使用流
students.entrySet().stream()                  
        .sorted(Map.Entry.comparingByValue()) // 排序
        .parallel()                     // 流的并行
        .forEach(System.out::println);  // 在并行中,不能保证顺序,得使用forEachOrdered
// 查找值
System.out.println(students.getOrDefault("刘",1232)); // 若键不存在,则返回defaultValue默认值 1232
// 计算模式
// 修改 把value值改为名字的长度
// computeIfAbsent 若没有Key或有Key无值,则计算值
students.computeIfAbsent("张三", (k) -> k.length() ); // 张三存在,但有值,无法修改
students.computeIfAbsent("刘六", (k) -> k.length() ); // 刘六不存在,可修改
// computeIfPresent 若有Key,则计算值,修改
students.computeIfPresent("张三", (k,v) -> k.length() ); // 张三存在,可修改
students.computeIfPresent("赵七", (k,v) -> k.length() ); // 赵七不存在,不可修改
// compute
students.compute("李四", (k,v) -> k.length() ); // 存不存在都可改
// replace
students.replace("李四", 123); //key,newValue
students.replace("张三",2,12); //key,oldValue,newValue
// merge
Map<String,Integer>student2 = new HashMap<>(); //新建一个map
student2.put("张三",11);
student2.put("关八",1);

student2.forEach((k,v)->
     students.merge(k,v,(s1,s2)->s1+s2)); // 合并,如果有相同key,则把value值相加
students.merge("李四",123,(k,v) -> v+1); //初始化检查 修改李四的值

4.ConcurrentHashMap

允许执行并发的添加和更新操作,其内部实现基于分段锁,相比于同步式的HashMap,读写性能更好。

方法描述
forEach对每个键或值对进行制定的操作
reduce规约计算
search根据键或值搜索
forEachKey/reduceKeys/searchKeys使用键
forEachValue/reduceValues/searchValues使用值
forEachEntry/reduceEntries/searchEntries使用Map.Entry对象
mappingCount返回映射数目long,不去用返回int的size方法,这样更具扩展性
keySet以Set形式返回视图
newKeySet创建一个由ConcurrentHashMap组成的Set

五、函数式编程

1.函数式编程的思考

函数式编程的思考
无论是行为参数化,还是Stream流,它们都与过去的面向对象编程有所区别,过去的编程风格也叫命令式编程,而我们在这里学到的是声明式编程
可以看出,命令式编程注重于“怎么做”,声明式编程更注重于“做什么”,因此可读性更好,写起来也方便,当然这不代表命令式编程彻底亡了。
总的来说,函数式编程要注意避免对共享数据的修改,包括不对输入参数直接使用,不用外部迭代遍历数据等。

2.函数式编程的技巧

函数式编程的技巧

主要分为两部分
(1)高阶函数和柯里化
高阶函数:输入和输出至少有一个函数
柯里化:f(x,y)=(g(x))(y)
(2)持久化数据结构
用创建和复制替代修改,避免共享数据的修改导致出错。


六、重构、测试和调试

在前面我们学习了Lambda表达式和Stream流,现在让我们来看看如何使用它们来重构代码

1.改善代码的可读性

(1) 实现类 -> 匿名类 -> Lambda表达式 -> 方法引用

有时候为了使用方法引用,要在类中添加方法(说实话,感觉也没那么方便)
用静态辅助方法,比如comparing和maxBy,sum和summingInt,替代写Lambda表达式

apples.sort((Apple a, Apple b) -> a1.getWeight().compareTo(a2.getWight()));
// 用我们的好工具!
apples.sort(Comparator.comparing(Apple::getWeight);

int total = menu.stream().map(Dish::getWeight)
						 .reduce(0,(w1,w2) -> w1 + w2);
// 用我们的好工具!
total = menu.stream().collect(summingInt(Dish::getWeight);
total = menu.stream().mapToInt(Dish::getWeight).sum();

(2) 命令式数据处理(外部迭代,比如for和if) -> 用Stream

2.增加代码的灵活性

(1) 使用函数式接口
(2) 有条件的延迟执行
当控制语句被混杂在业务逻辑代码之中

if (logger.isLoggable(Log.FINER)){ // isLoggable 获取日志状态 
	// 这样写把状态暴露给了客户端代码,而且每次执行都需要查询一次状态
	logger.finer("Problem:" + generateDiagnostic()); // 输出
}

简单说就是为了易读和封装性,在函数内部调查对象的状态,操作用行为参数

public viod log(Level level, Supplier<String> msgSupplier){
	if(logger.isLoggable(level)){
		log(level,msgSupplier.get());
	}
}

logger.log(Level.FINER, () -> "Problem:" + generateDiagnostic());

(3) 环绕执行
重用核心代码,让不同但基础相同的功能围绕着核心代码执行
例:一个文件读取的方法

@FunctionalInterface
public interface BufferedReaderProcessor{  // 用于传递BufferdReader参数
    String process(BufferedReader b) throws IOException;
}

// 传入一个 BufferedReader -> String
public static void processFile(String filePath,BufferedReaderProcessor bp) throws IOException{
    BufferedReader br = new BufferedReader(new FileReader(filePath));
    System.out.println(bp.process(br));
}

围绕着这个方法,可以实现不同的方法

// 读两行
processFile("src/main/java/file/data.txt", (BufferedReader br) -> br.readLine() + br.readLine());
// 读一行
processFile("src/main/java/file/data.txt", (BufferedReader br) -> br.readLine() );

3. 设计模式

(1) 策略模式
就是一个接口多种实现,有了Lambda后可以直接传方法实现了,跟前面行为参数化的思路差不多。

(2) 模版方法
采用某个算法的框架,但又想要一定的灵活度,所以对部分进行改进。
看个例子:

abstract class OnlineBanking {
	public void processCustomer(int id){
		Customer c = Database.getCustomerWithId(id);
		makeCustomerHappy(c);
	}
	abstract void makeCustomerHappy(Customer c);
}

在这里,processCustomer是框架,makeCustomerHappy是提供差异化的实现,用不同的银行继承OnlineBanking类,重写makeCustomerHappy。
这时候,我们可以用函数式接口和Lambda表达式:

public void processCustomer(int id, Cunsumer<Customer> makeCustomerHappy){
	Customer c = Database.getCustomerWithId(id);
	makeCustomerHappy.accept(c);
}

现在使用就不用继承了,直接传入一个方法实现差异化。
说白了就是对部分需要差异化的方法用函数式接口

(3) 观察者模式
某个事件发生时,一个对象(通常称为主题)要通知其他多个对象,就要用到这个模式。

在这里插入图片描述
简单说就是Subject要消费Observer,Observer里面的方法比较简单,也没有什么成员变量,一个方法是创建多个具体的Observer实现类,而用Lambda表达式可以不用Observer接口了,直接往Subject的方法里传具体的方法。
当然,不是什么时候都可以使用Lambda表达式,如果逻辑过于复杂,还是用类的方法

(4) 责任链模式
一种创建处理对象序列的方案,一个处理对象完成工作后,将结果传递给另一个对象,这个对象接着做一些工作,再传递给下一个对象。
传统的方法是创建对象,然后调用方法,将一个方法的结果传递给另一个,而使用Lambda表达式,可以直接用生成流水线,然后传入值。
如:

UnaryOperator<String> process1 = () -> {};
UnaryOperator<String> process2 = () -> {};
Function<String, String> pipeline = process1.andThen(process2);
String result = pipeline.apply("");

(5) 工厂模式
使用工厂模式,无须向客户暴露实例化的逻辑就能完成对对象的创建,例如一个水果产品工厂:

public class ProductFactory {
	public static Product createProduct(String name){
		switch(name){
			case"apple":return new Apple();
			case"banana":return new Banana();
			default:throw new RuntimeException("No such product:" + name);
		}
	}
}

使用工厂创建

Product p = ProductFactory.createProduct("apple");

而使用Lambda表达式:

Supplier<Product> appleSupplier = Apple::new;
Apple a = appleSupplier.get();

这样,你可以通过重构之前的代码,比如用一个Map代替

final static Map<String, Suppplier<String>> map = new HashMap<>();
static{map.put("apple", Apple::new); map.put("banana", Banana::new);} 

public static  Product createProduct(String name){
	Supplier<Product> p = map.get(name);
	if (p!=null) return p.get();
	throw new IllegalArgumentException("No such product:" + name);
}

这样只是一种用Lambda的写法,好不好用感觉也没那么好用,需要修改还是要修改

4.测试Lambda表达式

(1) 测试可见Lambda函数的行为
直接用,写到测试里
(2) 测试使用Lambda的方法的行为
关注包含Lambda表达式的行为,对其结果测试
(3) 将复杂的Lambda表达式分为不同的方法
把Lambda表达式转为方法引用,这样一般要新声明很多方法
(4) 高阶函数
高阶函数接收或者返回一个方法,我寻思测试还是对输入和输出的结果测呗,输出是方法就测这个方法呗

5.调试

(1) 栈跟踪
显然,由于Lambda没有函数名,那怎么跟踪呢?运行报错会有这样的信息:

at Debugging.lambda$main$0(Debugging.java:6) 
at Debugging$$lambda$5/284720968.apply(Unknown Source)

因为Lambda没有名字,这里的lambda$main$0就是编译器指定的一个名字。
lambda$5/284720968是使用了方法引用,比如Point::getX,这是一个类的方法,但无法跟踪到类。
如果指向的同一个类中声明的方法,那么是可以跟踪到的,比如:

class Debugging{
	public static void main(String[] args){
		xxx.map(Debugging::xxxx)
	}
}

(2) 使用日志
使用peek,对流的每个环节跟踪

List<Integer> result = numbers.stream()
							  .peek(x -> System.out.print(...))
							  .map()
							  .peek(x -> System.out.print(...))
							  .filter()
							  .peek(x -> System.out.print(...))
							  .collect(toList());

七、Java DSL

DSL(domain-specifc language,DSL),领域特定语言,它大多不通用,是为了某个特定领域客制化而生。
在过去,用Java创建的DSL并不多,但有了Lambda和Stream后,使用Java创建的DSL可读性大大提升。

1.DSL简介

DSL的意义什么?

引入DSL的目的是为了弥补程序员与领域专家对程序认知理解上的差异,程序员对业务逻辑不熟悉,而使用DSL可以让不熟悉编程的人也能阅读程序的逻辑并进行验证。

可见,DSL的意义就是让人看的懂,展现业务逻辑。
JVM已经提供了一些DSL的解决方案
(1) 内部DSL

list.forEach(new Consumer<String> (){
	@Override
	public void accpet(String s){
		Stream.out.println(s);
	}
});

在这段代码中,我们想传递的 “代码信号”list.forEachStream.out.println(s)
其他的代码都是语法噪声
而用Lambda表达式:

numbers.forEach(System.out::println); // 方法引用

这样看起来就十分清楚了

(2) 多语言DSL
实际上,JVM能支持很多语言,如Sclala和Groovy,Kotlin和Ceylon,有些语言的可读性更强,甚至没有语法噪声。
理论上,这些语言可以和Java兼容,但实际使用挺麻烦的,而且影响性能。
(3) 外部DSL
自己设计一个语言

2.现代Java API中的小型DSL

(1) 把Stream当成DSL去操作集合
示例:

List<String> errors = new ArrayList<>();
int errorcount = 0;
BufferedReader bufferedReader = new BufferedReader(new FileReader(fileName));
String line = bufferedReader.readLine();
while(errorCount < 10 && line != null) {
	if (line.startWith("ERROR")) {
		errors.add(line);
			erroeCount++;
	}
	line = buffuredReader.readLine();
}

用Stream改写

List<String> errors = Files.lines(Path.get(fileName))
						   .filter(line -> line.startWith("ERROR"))
						   .limit(10)
						   .collect(toList());

(2) 将Collectors作为DSL汇总数据
在之前我们学过,Collector可以支持嵌套的方式多层分组:

groupingBy(Apple::getWeight, groupingBy(Apple::getColor))

一般来说,流畅风格比嵌套可读性更好,你可以自己实现一个collectors构建器,实现流流畅的分组
但是,书上说实现不了

3. 使用Java创建DSL的模式与技巧

(1) 方法链接
通过构建器,可以实现方法的链接

Order order = forCustomer("hhhBank")
			  .buy(80)
			  .stock("IBM")
			     .on("NYSE"))
			   at(125.00)
			  .sell(50)
			  .stock("GOOGLE")
			     .on("NASDAQ"))
			  .at(37500)
			.end();

怎么实现的就省略了
想要这样的效果,需要构建很冗长的构建器,为了将顶层构建器与底层融合,要用到大量的胶水代码。
想想都不好写。

(2) 嵌套函数

Order order = order("hhhBank",
					buy(80,
						stock("IBM", on("NYSE")),
						at(125.00),
					sell(50,			  
						stock("GOOGLE",on("NASDAQ")),
						at(37500))
					);
// 实现
Builder{
Order order(String , Trade... )
Trade buy(int, Stock, double)
Trade sell(int, Stock, double)
Stock stock()
double at()
String on()
...
}

相比于方法链接,使用嵌套函数的层次结构更加清晰。
但是,括号比较多,参数列表也要严格预定。

(3) Lambda表达式函数序列

Order order = order( o->
	{o.forCustomer("hhhBank");
	 o.buy(t -> {
	 	t.quantity( 80 );
	 	t.price( 125.00 );
	 	t.stock( s -> {
	 		s.symbol( "IBM" );
	 		s.market( "NYSE );
	 });
	 o.sell(.....)
);	

这种方式既能流畅地表示逻辑,又能保留对象的层次结构。
不过,也有缺点,就是要写很多配置代码,在对象中要写很多接收Lambda表达式的接口。
(4) 把三个方法混合使用
(5) 使用方法引用

double value = new TaxCalculator().with(Tax::regional)
								  .with(Tax::surcharge)
								  .calculate(order);

跟Lambda一个道理,还是为了易读

4. Java DSL 的实际使用

几个例子
(1) 使用JOOQ DSL 读取数据库

Class.forName("xxx.Driver");
try(Connection c = getConnection(.....)){
	DSL.using(c)
		.slelect(...)
		.where(...)
	.orderBy(...)
	.fetch()
	.collect(groupingBy(...));
}

(2) 使用Spring Integraation DSL 配置一个Spring Integration工作流

@Bean
public IntegrationFlow myFlow() {
	return IntegrationFlows
				.from(...)
				.channel(...)
				.filter(...)
				.transform(...)
				.channel(...)
				.get(...);
}

八、并行与并发

如果你进行的是计算密集型的操作,并且没有I/O,那么推荐使用Stream并行。
如果设计I/O操作(还有网络等待),那么使用并发灵活性更好。

1.Java并行

Java并行

2.Java并发

Java并发
并发有点反人类,用的好的话可以用
还有个Java9的flow API,这里不写了


九、其他新的API

1.Optional类

Optional< T > 的目的是替代null,在以前为了检查返回值是否为null需要臃肿的检查,而使用Oprional来包裹返回结果,所有返回值都不会是null,只需要对Optional检查得知结果是否为null。

方法描述
empty创建一个空的Optional
of输入一个非空值,创建一个Optional
ofNullable创建一个Optional, 可接受空值
get如果变量存在,就返回,但不存在就会报错
orElse提供一个默认值,当不存在值的时候返回默认值
orElseGet当Optional不含值时才提供默认值,可以提升性能
orJava9引入的,为空时创建一个Optional返回
orElseThrow为空时抛出异常
ifPresent变量值存在时,执行传入的方法
map提取转换值,返回一个包裹传入方法返回值类型的Optional对象
flatMap链接Optional对象,传入一个返回值为Optional的方法,返回这个Optional对象
filter筛选

怎么Oprional也有map和filter?
来看一个例子:

public class Person{
    private Optional<Car> car;
    public Person(Optional<Car> car){this.car = car;}
    public Optional<Car> getCar(){return car;}
}

public class Car{
    private Optional<Insurance> insurance;
    public Car(Optional<Insurance> insurance){this.insurance = insurance;}
    public Optional<Insurance> getInsurance(){return insurance;}
}

public class Insurance{
    private String name;
    public Insurance(String name){this.name = name;}
    public String getName(){return name;}
    public Optional<String> geyOptionalName(){return Optional.ofNullable(this.name);}
}

可以看到,Person有个私有成员Optional< Car >,Car有个私有成员Optional< Insurance >,这就形成了Optional间的嵌套,假如我有个Person对象,我想获取它的Car的Insurance的name,应该怎么做?

Insurance insurance = new Insurance("中国");
Optional<Insurance> optionalInsurance = Optional.ofNullable(insurance);

Car car = new Car(optionalInsurance);
Optional<Car> optionalCar = Optional.ofNullable(car);

Person person = new Person(optionalCar);
Optional<Person> optionalPerson = Optional.ofNullable(person);

// 使用map提取转换值 获取Optional<Insurance>中Insurance的name
Optional<String> name = optionalInsurance.map(Insurance::getName);

// 使用flatMap链接Optional对象
String name_s = optionalPerson.flatMap(Person::getCar) // 获取Optional<Person>的Optional<CAR>
        .flatMap(Car::getInsurance) // 获取Optional<Car>的Optional<Insurance>
        .map(Insurance::getName)  // 使用map获取Oprional<x>的x的成员变量,并用Optional包装
        // 如果使用Insurance::geyOptionalName,返回的是Optional<Optional<String>>
        .orElse("Unknown");

// 使用flatMap链接Optional对象
name_s = optionalPerson.flatMap(Person::getCar)
        .flatMap(Car::getInsurance)
        .flatMap(Insurance::geyOptionalName)  // 用flatMap,接收Optional并返回
        .orElse("Unknown");             // 获取值

Optional<String> name_o = optionalPerson.flatMap(Person::getCar)
        .flatMap(Car::getInsurance)
        .flatMap(Insurance::geyOptionalName);

// 用filter判断Optional中变量是否符合条件
optionalInsurance.filter(insurance1 ->
        "日本".equals(insurance1.getName()))  // 如果不符合则返回一个空的Optional
        .ifPresent(x -> System.out.println("OK"));

// Java9提供了Optional和Stream的结合

有的时候,我们想要组合两个Optional,一种方式是解包

public Optional<Insurance> nullSafeFindCheapestInsurance( 
					Optional<Person> person, Optional<Car> car){
   if (person.isPresent() && car.isPresent()){
       return Optional.of(xxxx(person.get(), car.get()))
   }else{
       return Optional.empty();
   }
}

我们可以用不解包的方式

public Optional<Insurance> nullSafeFindCheapestInsurance( Optional<Person> person, Optional<Car> car){
    return person.flatMap(p -> car.map(c -> xxxx(p,c)));
}

2.时间API

因为比较简单,就直接拿例子来说了

// LocalDate
LocalDate localDate = LocalDate.now();
LocalDate date = LocalDate.of(2020,12,2);
date = LocalDate.parse("2022-02-22");

localDate.getYear();
localDate.getMonth();
localDate.getDayOfMonth();
localDate.lengthOfMonth();
localDate.isLeapYear();
// LocalTime
LocalTime time = LocalTime.now();
time = LocalTime.parse("12:21:21");

time.getHour();
time.getMinute();
time.getSecond();

// LocalDateTime
LocalDateTime dateTime =LocalDateTime.of(date,time);
dateTime = date.atTime(time);
dateTime = time.atDate(date);

date = dateTime.toLocalDate();
time = dateTime.toLocalTime();

/**
* 机器的日期和时间格式
* Instant 时间建模
* 以1970.1.1 开始秒数进行计算
*/
Instant.ofEpochSecond(3);
Instant.ofEpochSecond(3,11);
//Instant 设计的初衷是让机器使用

/**
* 通过Duration和Period使用Instant
* Duration 用于衡量秒,纳秒时间长短
* Period   用于衡量多个时间单位间长短
*/
LocalTime time1 = LocalTime.of(11,11,11);
LocalTime time2 = LocalTime.of(12,12,12);
Duration d1 = Duration.between(time1,time2);
System.out.println(d1.toMinutes());

Instant instant1 = Instant.ofEpochSecond(3);
Instant instant2 = Instant.ofEpochSecond(4);
Duration d2 = Duration.between(instant1,instant2);
System.out.println(d2.toNanos());

Period period = Period.between(LocalDate.of(2012,12,1),
                               LocalDate.of(2021,11,1));
System.out.println("year:" + period.getYears() + "month:" +period.getMonths() + "day:" + period.getDays());

/**
* 操作修改日期
*/
LocalDate date_test = LocalDate.of(2021,12,3);
date_test.withYear(2022);  // 改成2022
date_test.with(ChronoField.MONTH_OF_YEAR,2); // 改月份

date_test.plusWeeks(1); // 加一周
date_test.plus(6, ChronoUnit.MONTHS); // 加6个月
date_test.minus(6,ChronoUnit.MONTHS); // 减6个月
// atOffset 和某个时区偏移结合 atZone 和某个时区结合
/**
* 注:每个动作都会创建一个新的对象,因此date.xx 不会改变date, date = date.xx 才有效
*/
// TempoealAdjuster 可以进行更复杂的操作 这里先不写了
// 注:还有一些其他方法,这里先不写了

十、默认方法

传统上,Java接口是将相关方法组合在一起,实现接口的类必须为接口定义的每个方法提供一个实现,或者继承父类。但是,如果需要更新接口,这种方式就会出现问题,因为实体类往往不在接口设计者的控制范围内。
Java8提供了一些方法来解决这些问题,一是允许在接口内声明静态方法(static+method,只能由接口名.静态方法调用),二是引入默认方法,制定接口方法的默认实现。

default void sort(Comparator<? super E> c){
	Collections.sort(this,c);  
}

这是List的sort方法,可以看到default修饰符,表示这是个默认方法,那么,默认方法是用来干什么的?

1.不断演进的API

为了更好地理解默认方法是如何工作的,用一个例子来看一个API是如何不断演进的

第一版API

public interface Resizeable extends Drawable{ 
	int getWidth();
	int getHeight();
	void setWith(int width);
	void setHeight(int height);
	void setAbsoluteSize(int width, int height);
}

这是一个基本形状的接口。
这时候,有一个用户根据自己的需求实现了这个接口:

public class Ellipse implents Resizeble { // 一个新的形状
	......
}

然后,把这个接口用于一个小游戏:

public class Game {
	public static void main(String...args){
		List<Resizable> resizableShapes  // 图形列表
			= Arrays.aslist(new Square(),new Rectangle(),new Ellipse());
		Utils.paint(resizableShapes);   // 绘制图形
	}
}
public class Utils {
	public static void paint(List<Resizable> list){
		list.forEach(r -> {r.setAbsoluteSize(1,1); // 调用形状的方法
						   r.draw();});
	}
}

第二版API
用了一段时间后,我们需要进行更新

public interface Resizeable extends Drawable{ 
	int getWidth();
	int getHeight();
	void setWith(int width);
	void setHeight(int height);
	void setAbsoluteSize(int width, int height); 
	// 新的功能
	void setRelativeSize(int wFactor, int wFactor);
}

这时候,我们再去使用Game,调用Utils和Ellipse会编译出错,因为原有的接口已经改变,Ellipse中没有定义setRelativeSize方法

二进制级的兼容性:现有的二进制执行文件能无缝持续链接。如:接口新增方法,但不被调用,不会出错。(不编译Ellipse)
源代码级的兼容性:引入变化后,编译能通过。如:接口新增方法,编译无法通过,因为不是源码级兼容,遗留代码没有实现新引入的方法。
函数行为的兼容性:发生变更时,程序接收同样输入能得到同样输出。如:接口新增方法,因为新的方法未被调用,是函数行为兼容的。

2.使用默认方法

默认方法,简单说就是在接口中定义的方法,能被继承它的实现类默认拥有,不用再写一个实现了,比如Java8的removeIf方法,就是写在Collection接口里的,这样后面的List,Set,Map就不用再写具体实现了。
在这里插入图片描述

来看看如何使用默认方法吧
(1)可选方法
有的时候,一些接口不急着对一些方法写具体实现,所以就声明一下,然后空着,在以前需要对继承这些接口的接口继续声明,而现在使用默认方法就只用在最开始的接口里写了,很方便。
Iterator接口的remove方法就是这样
在这里插入图片描述

(2)行为的多继承

抽象类和抽象接口:
一个类只能继承一个抽象类,但是一个类可以实现多个接口
一个抽象类可以通过实例变量保存一个通用状态,而接口不能有实例变量

在这里插入图片描述
LinkedList继承了一个类,但是实现了多个接口,这样就可以实现多功能继承了。
在实际使用中,如何巧妙地设计多接口,是一个需要思考的问题,为了让代码更加灵活,对不同的功能进行分类细化,最后通过行为组合的方式实现。
例如,有三个接口:Rotatable(旋转),Moveable(移动),Resizable(缩放),Sun图形就只需要旋转和移动,Monster图形就三个都要。

(3)解决冲突
如果实现的接口和继承的类中有重名的方法,这时候该怎么办?
Java8设定了三条规则:
1.类或父类中的方法优先级最高。
2.若1无效,则子接口的优先级其次:假如类C实现了A接口和B接口,而B实现A,则使用B,因为B的实现比A更加具体。如果C实现了A和B,AB相互独立,则无法编译通过。
3.最后,还是无法判断,则显示地覆盖和调用期望的方法。

public C implements A,B{
	void hello(){
	    B.super.hello(); // 显式调用
	}
}

4.菱形继承问题
类D实现了B接口和C接口,而B和C都继承A接口,如果BC中没有对A的默认方法重写,那就使用A的;如果有就看2和3。

十一、模块系统

Java9引入的内容,这里暂时不写了。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值