Stream 流 【学习笔记】Java 基础


若文章内容或图片失效请留言反馈

部分素材来自网络若不小心影响到您的利益请联系博主删除

写这篇博客旨在制作笔记方便个人在线阅览巩固知识无其他用途


学习视频

  • 黑马 Java 基础教程Stream 流从入门到精通
  • 黑马程序员 Java 零基础视频教程】(上部
  • 黑马程序员 Java 零基础视频教程】(下部

Java 基础(查缺补漏)


  • Java 集合学习笔记Java 基础https://blog.csdn.net/yanzhaohanwei/article/details/129458827
  • Stream 学习笔记Java 基础https://blog.csdn.net/yanzhaohanwei/article/details/129258429

吐槽基础不牢地动山摇


【不可变集合】


1.不可变集合概述


不可变集合的特点

  • 不可变集合定义完成后不可以修改,或者进行添加、删除的操作

创建不可变集合

  • ListSetMap 接口中都存在静态的 of() 方法,可以获取一个不可变的集合
  • :这个 of() 方法在 JDK 9 版本才开始提供

三种集合使用 of() 创建不可变集合的细节

  • ListList 是有序存储元素的,存储的元素内容是可重复的。可以直接用。
  • SetSet 存储的元素的内容是无序的,也是不可重复的。
  • MapMap 存储的元素是不可重复的,键值对数量最多是 10 个。
    如果超过 10 个键值对(也就是超过了 20 个参数),就使用 Map.ofEntries()JDK 9 开始提供该方法)。
    也可以使用 Map.copyof()JDK 10 开始提供该方法)来处理上述的情况。
    HashMap 可以存储元素内容 nullkeyvalue,可以有多个值为 null ,但只能有一个键是 null

2.List 方式


2.1.List.of()


创建一个不可变集合

List<String> list = List.of("张三", "李四", "王五", "赵六");

对不可变集合做出修改其内部元素的操作,都会报错:java.lang.UnsupportedOperationException

list.remove("李四");
list.add("孙七");
list.set(0, "陈二");

2.2.遍历 List 集合


此处顺带回顾 List 集合的三种遍历方式


  • 增强 for 循环遍历
private static void ergodic_1(List<String> list) {
    for (String s : list) {
        System.out.print(s + "\t");
    }
}
  • 迭代器遍历
private static void ergodic_2(List<String> list) {
    Iterator<String> listIterator = list.iterator();
    while (listIterator.hasNext()) {
        String s = listIterator.next();
        System.out.print(s + "\t");
    }
}
  • 普通的 for 循环遍历
private static void ergodic_3(List<String> list) {
    for (int i = 0; i < list.size(); i++) {
        String s = list.get(i);
        System.out.print(s + "\t");
    }
}

使用上面的三种遍历方式,控制台输出信息都是如下所示

张三	李四	王五	赵六	

3.Set 方式


3.1.Set.of()


创建不可变集合(Set 中每一个元素都是独一无二的,不可存在重复的元素)

Set<String> set = Set.of("张三", "李四", "王五", "赵六");

修改不可变集合内部元素的操作,都会导致控制台报错:java.lang.UnsupportedOperationException

set.remove("王五");

3.2.遍历 Set 集合


这里顺带回顾一下 Set 集合的遍历方式


  • 增强 for 循环遍历
private static void ergodic_1(Set<String> set) {
    for (String s : set) {
        System.out.print(s + "\t");
    }
}
  • 迭代器遍历
private static void ergodic_2(Set<String> set) {
    Iterator<String> iterator = set.iterator();
    while (iterator.hasNext()) {
        String next = iterator.next();
        System.out.print(next + "\t");
    }
}

遍历结果(无序)

赵六	张三	王五	李四	

TreeSet 是有序的,是基于 TreeMapNavigableSet 实现的。

可以认为TreeSetTreeMap 都是基于红黑树实现的。

这些元素使用他们的自然排序或者在创建时提供的 Comparator 进行排序,具体取决于使用的构造函数。


4.Map 方式


4.1.Map.of()


修改不可变集合内部的元素,会报错:java.lang.UnsupportedOperationException

public class Immutable_MapDemo {
    public static void main(String[] args) {
        Map<String, String> map_1 = createMap_1();
        map_1.remove("李四");
    }

	private static Map<String, String> createMap_1() {
	    return Map.of(
	            "张三", "北京",
	            "李四", "上海",
	            "王五", "广州",
	            "赵六", "深圳"
	    );
	}
}

4.2.注意细节


  • 细节一Map 里的键是不可重复
  • 细节二Map 里面的 of 方法,参数是有上限的,最多只可传递 20 个参数即 10 个键值对
  • 细节三如果我们要传递多个键值对对象,它的数量大于 10 个,可以将键值对视为一个整体传递给 entries

键重复,在主方法中调用下面的方法会抛异常:java.lang.IllegalArgumentException: duplicate key: XXX

private static Map<String, String> createMap_2() {
    return Map.of(
            "张三", "北京",
            "张三", "上海",
            "王五", "广州",
            "赵六", "深圳"
    );
}

Map 里面的 of 方法,参数是有上限的,最多只可传递 10 个键值对

private static Map<String, String> createMap_3() {
    return Map.of(
            "刘一", "北京", "李二", "上海", "张三", "广州", "赵四", "深圳", "王五", "北京",
            "赵六", "上海", "孙七", "广州", "周八", "深圳", "吴九", "北京", "郑十", "上海"
            // 再添加一个[键值对],编译器就会报错了
    );
}

如果我们要传递多个键值对对象,它的数量大于 10 个,可以将键值对视为一个整体传递给 entries

那么此时我们有这样的一个需求:一个方法可以接受多个键,多个值

解决方案:键和值都是可变参数,泛型方法(在传入的参数类型不确定的情况下)

public static <K, V> void of(K... keys, V... values) { } // 直接报错

然而这样的写法,会报错:Vararg parameter must be the last in the list

对于可变参数而言:在一个方法里,只能有一个形参是可变参数,而且必须写在最后

此时,我们便需要使用方法 Map.ofEntries() 了(JDK 9 开始提供该方法)


4.3.Map.ofEntries()


java/util/Map.offEntries()

@SafeVarargs // 该注解抑制编译器警告
static <K, V> Map<K, V> ofEntries(Map.Entry<? extends K, ? extends V>... entries) {
    if (entries.length == 0) {
        return ImmutableCollections.emptyMap();
    } else if (entries.length == 1) {
        return new Map1(entries[0].getKey(), entries[0].getValue());
    } else {
        Object[] kva = new Object[entries.length << 1];
        int a = 0;
        Map.Entry[] var3 = entries;
        int var4 = entries.length;

        for(int var5 = 0; var5 < var4; ++var5) {
            Map.Entry<? extends K, ? extends V> entry = var3[var5];
            kva[a++] = entry.getKey();
            kva[a++] = entry.getValue();
        }

        return new MapN(kva);
    }
}

4.3.1.基本使用


创建一个普通的 map 集合(HashMap

private static HashMap<String, String> createHashMap() {
    HashMap<String, String> hashMap = new HashMap<>();
    hashMap.put("刘一", "北京");
    hashMap.put("李二", "上海");
    hashMap.put("张三", "广州");
    hashMap.put("赵四", "深圳");
    hashMap.put("王五", "天津");
    hashMap.put("赵六", "重庆");
    hashMap.put("孙七", "成都");
    hashMap.put("周八", "贵州");
    hashMap.put("吴九", "昆明");
    hashMap.put("郑十", "南宁");
    hashMap.put("Key_11", "Value_11");
    return hashMap;
}

利用上面的数据来创建一个不可变的集合

private static void getImmutable_1() {
    HashMap<String, String> hashMap = createHashMap(); 

    Set<Map.Entry<String, String>> entries = hashMap.entrySet();

    Map.Entry[] entries_1 = entries.toArray(new Map.Entry[0]);
    Map map = Map.ofEntries(entries_1);

    Set<Map.Entry<String, String>> entries_tmp = map.entrySet();
    for (Map.Entry<String, String> entry : entries_tmp) {
        System.out.println(entry); // 正常遍历。此处我就不贴遍历的信息了
    }
}

修改不可变集合内部的元素,会报错:java.lang.UnsupportedOperationException

map.put("NewKey", "NewValue");

可以用链式编程的书写方法来简化上面的代码

Map<Object, Object> immutableMap_2 = Map.ofEntries(createHashMap().entrySet().toArray(new Map.Entry[0]));

4.3.2.理解 toArray(new Map.Entry[0])


关于 Map.Entry[] entries_1 = entries.toArray(new Map.Entry[0]); 这行代码我们可以理解为如下的情况

Map.Entry[] entries_a = new Map.Entry[0];
Map.Entry[] entries_b = entries.toArray(entries_a);

  • toArray():把 entries 变成一个 object[] 数组
HashMap<String, String> hashMap = createHashMap();
Set<Map.Entry<String, String>> entries = hashMap.entrySet();

Object[] objects = entries.toArray();

我们的需求是获取指定类型的数组,这里我们用不到这个无参方法。

// 遍历对象数组:Object[] 
System.out.println("entries.toArray():");
for (Object object : objects) {
    System.out.println("\t" + object); 
}

  • toArray(T[] a):把 entries 变成一个数组,这个数组的类型是可以被指定的
HashMap<String, String> hashMap = createHashMap();
Set<Map.Entry<String, String>> entries = hashMap.entrySet();

Map.Entry[] arrays_a = new Map.Entry[0];
Map.Entry[] arrays_b = entries.toArray(arrays_a);

带参数的这个方法是符合我们的要求的,可以指定数组类型


注意toArray 方法在底层会比较 [数组的长度] 与 [集合的长度] 二者的大小

  • [集合的长度] > [数组的长度]:数据在数组中放不下,此时会根据实际数据的个数重新创建数组
  • [集合的长度] <= [数组的长度]:数据在数组中是放得下的,此时不会创建新的数组,直接用
private static void getImmutable_2() {
    HashMap<String, String> hashMap = createHashMap();
    Set<Map.Entry<String, String>> entries = hashMap.entrySet();

    System.out.println("Map.Entry[].size():");
    
    Map.Entry[] arrays1_1 = new Map.Entry[0];
    Map.Entry[] arrays1_2 = entries.toArray(arrays1_1);
    System.out.println("\t" + "Map.Entry[0].length:" + arrays1_2.length); // 11

    Map.Entry[] arrays2_1 = new Map.Entry[20];
    Map.Entry[] arrays2_2 = entries.toArray(arrays2_1);
    System.out.println("\t" + "Map.Entry[20].length:" + arrays2_2.length); // 20
}

在主方法中调用上方代码块中的方法,运行 IDEA,控制台输出信息如下

Map.Entry[].size()Map.Entry[0].length:11
	Map.Entry[20].length:20

4.3.3.HashMap 注意事项


HashMap 可以存储 nullkeyvalue,但 null 作为键只能有一个,null 作为值可以有多个

private static Map createMap_4() {
    HashMap<String, String> hashMap = new HashMap<>();
    hashMap.put("诸葛亮", "琅琊");
    // 下面三个键值对,只会存入一个
    hashMap.put("", "");
    hashMap.put("", "杭州");
    hashMap.put("", "南京");
    hashMap.put("司马懿", "河内");
    return hashMap;
}

在主方法中调用上方代码块中的方法,并遍历 Map 集合

public static void main(String[] args) {
    Map map_4 = createMap_4();
    Set<Map.Entry<String, String>> entries = map_4.entrySet();
    for (Map.Entry<String, String> entry : entries) {
        System.out.println(entry);
    }
}

控制台打印信息(显然,HashMap 存储的键值对是无序的,而且只能有一个键为 null 的键值对)

=南京
司马懿=河内
诸葛亮=琅琊

4.4.Map.copyof()


HashMap<String, String> hashMap = createHashMap(); 
Set<Map.Entry<String, String>> entries_0 = hashMap.entrySet();
Map.Entry[] entries_1 = entries_0.toArray(new Map.Entry[0]);
Map map = Map.ofEntries(entries_1);

简化上方的代码块(链式编程)

HashMap<String, String> hashMap = createHashMap();
Map<Object, Object> entries_2 = Map.ofEntries(hashMap.entrySet().toArray(new Map.Entry[0]));

此时再让我们看看 java/util/Map.copyOf() 中内部的情况,几乎与上方的简化代码如出一辙

static <K, V> Map<K, V> copyOf(Map<? extends K, ? extends V> map) {
    return map instanceof AbstractImmutableMap ? map : ofEntries((Map.Entry[])map.entrySet().toArray(new Map.Entry[0]));
}

所以此时我们可以直接用这个 Map.copyOf() 来创建不可变集合(参数超过 20 个)

private static void getImmutable_4() {
    HashMap<String, String> hashMap = createHashMap();
    Map<String, String> immutableMap = Map.copyOf(hashMap);
}

修改不可变集合内部的元素,会报错:java.lang.UnsupportedOperationException

immutableMap.put("NewKey", "NewValue"); 

4.5.遍历 Map 集合


这里顺带回顾一下 Map 集合的遍历方式


第一种遍历方式

private static void ergodic_1(Map<String, String> map) {
    Set<String> keys = map.keySet();
    for (String key : keys) {
        String value = map.get(key);
        System.out.println(key + ":" + value);
    }
}

第二种遍历方式

private static void ergodic_2(Map<String, String> map) {
    Set<Map.Entry<String, String>> entries = map.entrySet();
    for (Map.Entry<String, String> entry : entries) {
        String key = entry.getKey();
        String value = entry.getValue();
        System.out.println(key + ":" + value);
}

【Stream 流】


1.Stream 概述


  • Stream 流的作用:结合了 Lambda 表达式,简化集合、数组的操作

  • Stream 流的使用步骤
    • 先得到一条 Stream 流(流水线),并把数据放上去
    • 利用 Stream 流中的 API 进行各种操作
      • 使用 中间方法 对流水线上的数据进行操作
      • 使用 终结方法 对流水线上的数据进行操作

2.获取 Stream 流


获取方式 方法名 说明
单列集合 default Stream stream() Collection 中的默认方法
双列集合 无法直接使用 stream
数组 public static Stream stream(T[] array) Arrays 工具类中的静态方法
一堆零散数据 public static Stream of(T... values) Stream 接口中的静态方法

2.1.单列集合


创建一个单列集合

ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "a", "b", "c", "d", "e");

可以采用内部类的方式来遍历

Stream<String> stream_1 = list.stream();
stream_1.forEach(new Consumer<String>() {
    @Override
    public void accept(String s) {
        System.out.print(s);
    }
});

使用链式编程的书写方式更符合 Stream 流的思想

list.stream().forEach(s -> System.out.print(s));

还可以进一步简化上面一行的代码

list.stream().forEach(System.out::print);

打印结果

abcde

2.2.双列集合


创建一个双列集合

HashMap<String, Integer> hashMap = new HashMap<>();
hashMap.put("A", 111);
hashMap.put("B", 222);
hashMap.put("C", 333);
hashMap.put("D", 444);
hashMap.put("E", 555);

第一种遍历方式

hashMap.keySet().stream().forEach(s -> System.out.print(s));

打印结果

ABCDE

第二种遍历方式

hashMap.entrySet().stream().forEach(s-> System.out.println(s));

打印结果

A=111
B=222
C=333
D=444
E=555

2.3.数组


数组是基本数据类型

int[] array = {1, 2, 3, 4, 5, 6, 7, 8, 9};

Arrays.stream(array).forEach(s -> System.out.print(s));

数组是引用数据类型

String[] strings = {"a", "b", "c", "d"}; 

Arrays.stream(strings).forEach(s -> System.out.print(s));

2.4.一堆零散的数据


可以使用 Stream 流,但必须是同一类型

Stream.of(1, 2, 3, 4, 5).forEach(s -> System.out.println(s));
Stream.of("a", "b", "c", "d", "e").forEach(s -> System.out.println(s));

2.5.注意事项


public class StreamDemo_3S {
    public static void main(String[] args) {
        int[] ints = {1, 2, 3, 4};
        Integer[] integers = {1, 2, 3, 4};
        String[] strings = {"a", "b", "c", "d"};
        System.out.println("================================");
        Stream.of(ints).forEach(s -> System.out.print(s));
        System.out.println("\n" + "================================");
        Stream.of(integers).forEach(s -> System.out.print(s));
        System.out.println("\n" + "================================");
        Stream.of(strings).forEach(s -> System.out.print(s));
        System.out.println("\n" + "================================");
    }
}

输出结果

================================
[I@7ef20235
================================
1234
================================
abcd
================================

java/util/stream/Stream.java

static <T> Stream<T> of(T... values) {
    return Arrays.stream(values);
}

Stream 流的接口中的静态方法 of() 的细节:

  • 该方法的形参是一个可变参数,可以传递一堆零散的数据,也可以传递数组。
  • 数组一定要是引用类型才可以正常输出我们想要的值。
  • 如果传递的数据是基本数据类型,则会把整个数组当成一个元素,放到 stream

3.Stream 流的中间方法


名称 说明
Stream filter(Predicate predicate) 过滤
Stream limit(long maxSize) 获取前几个元素
Stream skip(long n) 跳过前几个元素
Stream disitinct() 元素去重,依赖(hashCodeequals 方法)
static Stream concat(Stream a, Stream b) 合并 ab 两个流成为一个流
Stream map(Function mapper) 转换流中的数据类型

3.1.filter()


Stream<T> filter(Predicate<? super T> predicate); // 对流中的数据进行过滤操作

Predicate 接口中的泛型 T 是在流中,当前接收的数据类型

boolean test(T t);

Predicate 接口中有一个方法:boolean test(T t),其会对给定的参数进行判断,返回一个布尔值

  • 若返回值为 true,则表示当前数据留下
  • 若返回值为 false,则表示当前数据舍弃不要

创建一个数组

ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌", "赵敏", "周芷若", "张强", "张三丰", "张翠山", "张良", "王二麻子", "谢广坤");

为了便于理解,这里先使用内部类的方式对数据过滤

list.stream().filter(new Predicate<String>() {
    @Override
    public boolean test(String s) {
        return s.startsWith("张");
    }
}).forEach(s -> System.out.println(s));

当然,一般更建议使用 链式编程 + Lambda 的书写方式来写 Stream 流(可以理解为 filter(参数 -> boolean 值)

list.stream().filter(s -> s.startsWith("张")).forEach(s -> System.out.println(s));

控制台打印信息

张无忌
张强
张三丰
张翠山
张良

java/util/function/Predicate.java 源码

@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);
    
    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

    default Predicate<T> negate() {
        return (t) -> !test(t);
    }

    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }

    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }
}

3.2.中间方法注意事项


注意事项

  • 中间方法会返回新的 Stream 流,原来的流会被关闭。
    这样一来,原来的 Stream 流只能使用一次,故建议使用链式编程
  • 修改 Stream 流中的数据,不会影响原来集合或数组中的数据

3.2.1.注意-1


注意-1:中间方法会关闭之前的 Stream 流,返回新的 Stream 流。


创建一个数组

ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌", "赵敏", "周芷若", "张强", "张三丰", "张翠山", "张良", "王二麻子", "谢广坤");

创建完数组后,再过滤信息

Stream<String> stream_1 = list.stream().filter(s -> s.startsWith("张"));

Stream<String> stream_2 = stream_1.filter(s -> s.length() == 3);

Stream<String> stream_3 = stream_1.filter(s -> s.length() == 3);

控制台输出信息:显然,报错了(stream has already been operated upon or closed

Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
	at java.util.stream.AbstractPipeline.<init>(AbstractPipeline.java:203)
	at java.util.stream.ReferencePipeline.<init>(ReferencePipeline.java:94)
	at java.util.stream.ReferencePipeline$StatelessOp.<init>(ReferencePipeline.java:618)
	at java.util.stream.ReferencePipeline$2.<init>(ReferencePipeline.java:163)
	at java.util.stream.ReferencePipeline.filter(ReferencePipeline.java:162)
	at org.example.test1.StreamDemo_6.method_2(StreamDemo_6.java:24)
	at org.example.test1.StreamDemo_6.main(StreamDemo_6.java:15)

解决办法便是使用链式编程一步到位。为了便于阅读,一般会折行每行代码

list.stream()
        .filter(s -> s.startsWith("张"))
        .filter(s -> s.length() == 3)
        .forEach(s -> System.out.println(s));

控制台输出信息(张姓,且名字的字数为三)

张无忌
张三丰
张翠山

3.2.2.注意-2


注意-2:修改 Stream 流中的数据,不会影响原来集合或数组中的数据


System.out.println("------------------------------------------------------------------------------------");
list.stream()
        .filter(s -> s.startsWith("张"))
        .filter(s -> s.length() == 3)
        .forEach(s -> System.out.println(s));
System.out.println("------------------------------------------------------------------------------------");
System.out.println(list);
System.out.println("------------------------------------------------------------------------------------");

控制台输出信息(显然,原数组中的信息并没有受到影响)

------------------------------------------------------------------------------------
张无忌
张三丰
张翠山
------------------------------------------------------------------------------------
[张无忌, 赵敏, 周芷若, 张强, 张三丰, 张翠山, 张良, 王二麻子, 谢广坤]
------------------------------------------------------------------------------------

3.3.limit()


Stream<T> limit(long maxSize) // 返回此流中的元素组成的流,截取前指定参数个数的数据

ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌", "赵敏", "周芷若", "张强", "张三丰", "张翠山", "张良", "王二麻子", "谢广坤");

list.stream()
        .limit(3)
        .forEach(s -> System.out.print(s + " "));

控制台输出信息

张无忌 赵敏 周芷若 

3.4.skip()


Stream<T> skip(long n) // 跳过指定参数个数的数据,返回由该流的剩余元素组成的流

ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌", "赵敏", "周芷若", "张强", "张三丰", "张翠山", "张良", "王二麻子", "谢广坤");

list.stream()
        .skip(3)
        .forEach(s -> System.out.print(s + " "));

控制台输出信息

张强 张三丰 张翠山 张良 王二麻子 谢广坤 

3.5.distinct()


Stream<T> distinct() // 返回由该流的不同元素(根据 Object.equals(Object))组成的流,即去重

ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌", "张无忌", "张无忌", "张无忌", "赵敏", "周芷若", "张强", "张三丰", "张翠山", "张良", "王二麻子", "谢广坤");

list.stream().distinct().forEach(s -> System.out.print(s + " ")); 

输出结果

张无忌 赵敏 周芷若 张强 张三丰 张翠山 张良 王二麻子 谢广坤 

3.6.concat()


static <T> Stream<T> concat(Stream a, Stream b) // 合并 a 和 b 两个流成为一个流

ArrayList<String> list_1 = new ArrayList<>();
Collections.addAll(list_1, "张无忌", "张无忌", "张无忌", "张无忌", "赵敏", "周芷若", "张强", "张三丰", "张翠山", "张良", "王二麻子", "谢广坤");

ArrayList<String> list_2 = new ArrayList<>();
Collections.addAll(list_2, "周芷若", "赵敏", "小昭");

Stream.concat(list_1.stream(), list_2.stream()).forEach(s -> System.out.print(s + " "));

控制台输出信息

张无忌 张无忌 张无忌 张无忌 赵敏 周芷若 张强 张三丰 张翠山 张良 王二麻子 谢广坤 周芷若 赵敏 小昭 

3.7.map()


Stream<R> map(Function<T, R> mapper) // 转换流中的数据类型,即 T -> R
  • Function 中的第一个参数 T:是流中原本的数据类型
  • Function 中的第二个参数 R:是要转成的类型
R apply(T t);

Function 接口中有一个方法:R apply(T t)

  • R apply(T t) 中的形参 t:依次表示流里面的每一个数据
  • R apply(T t) 中的返回值:表示转换之后的数据
  • R apply(T t) 方法体的作用:接收一个 T 类型的参数,返回一个 R 类型的值

ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌-15", "周芷若-14", "赵敏-13", "张强-20", "张三丰-100", "张翠山-40", "张良-35", "王二麻子-37");

需求:只获取里面的年龄,并进行打印

为了便于理解,这里先使用内部类的方式对数据进行操作

list.stream()
        // Function 中的第一个参数 T:流中原本的数据类型
        // Function 中的第二个参数 R:要转成的类型
        .map(new Function<String, Integer>() {
                 // apply() 中的形参 s:依次表示流里面的每一个数据
                 // apply() 中的返回值:表示转换之后的数据
                 @Override
                 public Integer apply(String s) {
                     String[] arrays = s.split("-");
                     String ageString = arrays[1];
                     int age = Integer.parseInt(ageString);
                     return age;
                 }
             }
        ).forEach(s -> System.out.print(s + " "));

Lambda 表达式方式(可以理解为 map(参数 -> 结果),这个结果可以由逻辑操作得出,比如类型转换)

list.stream()
        .map(s -> Integer.parseInt(s.split("-")[1]))
        .forEach(s -> System.out.print(s + " "));

控制台输出信息

15 14 13 20 100 40 35 37 

java/util/function/Function.java 源码

@FunctionalInterface
public interface Function<T, R> {

    R apply(T t);

    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

4.Stream 流的终结方法


名称 说明
void forEach(Consumer action) 遍历
long count() 统计
toArray() 收集流中的数据,放到数组中
collect(Collector collector) 收集流中的数据,放到集合中

4.1.forEach()


java/util/stream/Stream.java 中的 forEach 方法

void forEach(Consumer<? super T> var1); // 遍历流中的数据

Consumer 接口中有一个方法:accept(T var1)

void accept(T var1);
  • Consumer 中的泛型:表示流中的数据类型
  • Consumeraccept(T var1) 方法的形参 var1:依次表示流里的每一个数据
    (类似于 for(String s:strings) 中的 s
  • Consumer 中的 accept(T var1) 方法体的作用:对每一个数据的处理操作(比如说:打印操作)

ArrayList<String> list = new ArrayList<>();

Collections.addAll(list, "张无忌", "赵敏", "周芷若", "张强", "张三丰", "张翠山", "张良", "王二麻子", "谢广坤");

为了便于理解,这里先使用内部类的方式对数据进行操作

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

Lambda 表达式方式(可以理解为 forEach(参数 -> void),可以做不返回结果的操作,比如打印操作)

list.stream().forEach(s -> System.out.println(s));

java/util/function/Consumer.java 源码

@FunctionalInterface
public interface Consumer<T> {
    void accept(T var1);

    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (t) -> {
            this.accept(t);
            after.accept(t);
        };
    }
}

4.2.count()


long count() // 统计流中的数据的个数

ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌-15", "周芷若-14", "赵敏-13", "张强-20", "张三丰-100", "张翠山-40", "张良-35", "王二麻子-37");
long count = list.stream().count();
System.out.println(count); // 输出结果:8

4.3.toArray()


toArray() // 收集流中的数据,放到数组中

在带参数的 toArray 方法中,有一个接口:IntFunction

@FunctionalInterface
public interface IntFunction<R> {
    R apply(int value);
}

IntFunction 接口中的 R apply(int value);Function 接口中的 R apply(T t) 并没有什么区别。

只不过这次的返回类型就是 IntFunction 接口中的唯一一个 R 类型。

  • 对于 R apply(int value); 这个抽象方法,可以理解为 (参数) -> 结果
    说的再具体点的话就是:(参数) -> (一个有返回值的操作)

结合 A[] toArray(IntFunction generator) 来看,即传入数据,对流中的数据逐个操作,并返回指定类型的数组,这个数组的长度也是要和流中的数据的个数相等的。

这么说或许有点小抽象,下面将结合例子具体分析。


创建一个数组

ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌-15", "周芷若-14", "赵敏-13", "张强-20", "张三丰-100", "张翠山-40", "张良-35", "王二麻子-37");

  • Object[] toArray():返回 Object[] 类型的数组
Object[] array_1 = list.stream().toArray();

System.out.println(Arrays.toString(array_1));

内部类方式使用 toArray 方法

/*
 * IntFunction 的泛型:具体类型的数组
 *
 * * apply() 的形参:流中数据的个数,要和数组的长度保持一致
 * * apply() 中的返回值:具体类型的数组
 * * apply() 方法体:创建数组
 * *
 * * toArray() 方法的参数的作用:负责创建一个指定类型的数组
 * * toArray() 方法的底层,会依次得到流里面的每一个数据,并且把数据放到数组中
 * * toArray() 方法的返回值:是一个装着流里面所有数据的数组
 */
String[] strings = list.stream().toArray(new IntFunction<String[]>() {
    @Override
    public String[] apply(int value) {
        return new String[value];
    }
});

System.out.println(Arrays.toString(strings));

Lambda 表达式方式使用 toArray 方法

(可以理解为 toArray(参数 -> 一个返回结果是数组的操作),这个操作是对流中的数据一对一操作的)

String[] strings = list.stream().toArray(value -> new String[value]);

System.out.println(Arrays.toString(strings));

4.4.collect()


java/util/stream/Stream.java 中的 collect() 方法之一

<R, A> R collect(Collector<? super T, A, R> collector);

collect(Collector collector) 中的这个 Collector 也是一个接口

这里贴一下 Collector 接口的内部结构图(JDK 11 版本)

Stream 流 【学习笔记】Java 基础_第1张图片

  • public staticCollectors toList():把元素收集到 List 集合中
  • public staticCollectors toSet():把元素收集到 Set 集合中
  • public static Collectors toMap(Function keyMapper, Function valueMapper)
    把元素收集到 Map 集合中

先创建一个 List 集合

public class StreamDemo_9_2 {
    public static void main(String[] args) {
        List<String> list = createList();
        // collectToList(list);
        // collectToSet(list);
        // collectToMap_1(list);
        // collectToMap_2(list);
    }

    private static List<String> createList() {
        List<String> list = new ArrayList<>();
        Collections.addAll(list,
                "张无忌1-男-15", "张无忌2-男-15", "周芷若-女-14", "赵敏-女-13", "张强-男-20",
                "张三丰-男-100", "张翠山-男-40", "张良-男-35", "王二麻子-男-37", "谢广坤-男-41");
        return list;
    }
}

4.4.1.toList()


private static void collectToList(List<String> list) {
    // 收集 List 集合中的所有男性
    List<String> newList = list.stream()
            .filter(s -> "男".equals(s.split("-")[1]))
            .collect(Collectors.toList());

    System.out.println(newList);
}

4.4.2.toSet()


private static void collectToSet(List<String> list) {
    Set<String> newSet = list.stream()
            .filter(s -> "男".equals(s.split("-")[1]))
            .collect(Collectors.toSet());

    System.out.println(newSet);
}

4.4.3.toMap()


Map 集合中,一定要指明谁作为键,谁作为值。

  • toMap(Function keyMapper, Function valueMapper)
    • 参数一 keyMapper:表示键的生成规则
    • 参数二 valueMapper:表示值的生成规则

java/util/function/Function.java

public interface Function<T, R> {
    R apply(T t); // 这是一个抽象方法,简单描述就是接收 T 类型的数据,返回 R 类型的数据
}
  • toMap(Function keyMapper, Function valueMapper)
  • 参数一
    • Function keyMapper
      • 第一个泛型 T,表示流中的每一个数据的类型
      • 第二个泛型 K,表示 Map 集合中的数据类型
    • R apply(T t)
      • 形参:表示流里面的每一个数据
      • 方法体功能:生成键的代码(
      • 返回值:已经生成好的键
  • 参数二
    • Function valueMapper
      • 第一个泛型 T,表示流中的每一个数据的类型
      • 第二个泛型 K,表示 Map 集合中的数据类型
    • R apply(T t)
      • 形参:表示流里面的每一个数据
      • 方法体功能:生成值的代码
      • 返回值:已经生成好的值

Map 集合中,一定要指明谁作为键,谁作为值。这里我指定姓名为键,年龄为值

内部类方式书写 toMap()

private static void collectToMap_1(List<String> list) {
    Map<String, Integer> map = list.stream()
            .filter(s -> "男".equals(s.split("-")[1]))
            .collect(Collectors.toMap(
                    new Function<String, String>() {
                        @Override
                        public String apply(String s) {
                            return s.split("-")[0];
                        }
                    },
                    new Function<String, Integer>() {
                        @Override
                        public Integer apply(String s) {
                            return Integer.parseInt(s.split("-")[2]);
                        }
                    }
            ));

    System.out.println(map);
}

Lambda 方式书写 toMap()

private static void collectToMap_2(List<String> list) {
    Map<String, Integer> map = list.stream()
            .filter(s -> "男".equals(s.split("-")[1]))
            .collect(Collectors.toMap(
                    s -> s.split("-")[0],
                    s -> Integer.parseInt(s.split("-")[2])
            ));

    System.out.println(map);
}

5.总结


Stream 流的作用

  • 结合了 Lambda 表达式,简化集合、数组的操作

Stream 的使用步骤

  • 获取 Stream 流对象
  • 使用中间方法处理数据
  • 使用终结方法处理数据

如何获取 Stream 流对象

  • 单例集合:Collection 中的默认方法 stream
  • 双列集合:不能直接获取
  • 数组:Arrays 工具类型中的静态方法 stream
  • 一堆零散的数据:Stream 接口中的静态方法 of

常用方法

  • 中间方法:filterlimitskipdistinct、concat、map
  • 终结方法:forEachcountcollect

6.练习


6.1.数字过滤


定义一个集合,并添加一些整数:1、2、3、4、5、6、7、8、9、10

过滤奇数,只留下偶数,并且需要将集合保存起来


public class Practice_1 {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        Collections.addAll(list, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        List<Integer> collect = list.stream().filter(s -> s % 2 == 0).collect(Collectors.toList());
        System.out.println(collect);
    }
}

控制台输出信息

[2, 4, 6, 8, 10]

6.2.字符串过滤并收集


创建一个 ArrayList 集合,并添加以下字符串,字符串的前面是姓名,后面是年龄

"zhangsan,23"
"lisi,24"
"wangwu,25"

保留年龄大于等于 24 岁的人,并将结果收集到 Map 集合中,以姓名为键,以年龄为值。


public class Practice_2 {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        Collections.addAll(list, "zhangsan,23", "lisi,24", "wangwu,25");

        Map<String, Integer> collect = list.stream()
                .filter(s -> Integer.parseInt(s.split(",")[1]) >= 24)
                .collect(Collectors.toMap(
                        s -> s.split(",")[0],
                        s -> Integer.parseInt(s.split(",")[1])
                ));

        System.out.println(collect);
    }
}

控制台输出

{lisi=24, wangwu=25}

6.3.自定义对象过滤并收集


现在有两个 ArrayList 集合

  • 第一个集合中:存储 6 名男演员的名字和年龄。
  • 第二个集合中:存储 6 名女演员的名字和年龄。

要求完成如下的操作

  1. 男演员只要名字为 3 个字的前两人
  2. 女演员只要姓杨的,并且不要第一个
  3. 把过滤后的男演员姓名和女演员姓名合并到一起
  4. 将上一步的演员信息封装成 Actor 对象
  5. 将所有的演员对象都保存到 List 集合中。

备注:演员类 Actor,其属性有 nameage


Actor

public class Actor {
    private String name;
    private int age;

    public Actor() {
    }

    public Actor(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

测试类

public class Practice_3 {
    public static void main(String[] args) {
        ArrayList<String> manList = new ArrayList<>();
        ArrayList<String> womanList = new ArrayList<>();

        Collections.addAll(manList, "蔡坤坤,24", "叶齁贤,23", "刘不甜,22", "吴签,24", "谷嘉,30", "肖梁梁,27");
        Collections.addAll(womanList, "赵小颖,35", "杨莹,36", "高元元,43", "张天天,31", "刘诗,35", "杨小幂,33");

        // 需求 1:男演员只要名字为 3 个字的前两人
        Stream<String> manStream = manList.stream()
                .filter(s -> s.split(",")[0].length() == 3)
                .limit(2);

        // 需求 2:女演员只要姓杨的,并且不要第一个
        Stream<String> womanStream = womanList.stream()
                .filter(s -> s.split(",")[0].startsWith("杨"))
                .skip(1);

        // 需求 3:把过滤后的男演员姓名和女演员姓名合并到一起
        // 需求 4:将上一步的演员信息封装成 Actor 对象
        // 需求 5:将所有的演员对象都保存到 List 集合中
        List<Actor> collect = Stream.concat(manStream, womanStream)
                .map(s -> new Actor(s.split(",")[0], Integer.parseInt(s.split(",")[1])))
                .collect(Collectors.toList());

        System.out.println(collect);
    }
}

控制台输出信息

[Actor{name='蔡坤坤', age=24}, Actor{name='叶齁贤', age=23}, Actor{name='杨小幂', age=33}]

【补充】


补充一:Lambda 表达式


1.1.Lambda 表达式概述


  • 面向对象:先找对象,让对象做事情
  • 函数式编程(Functional programming)思想,忽略面向对象的复杂语法
    强调做什么而不是谁去做

Lambda 表达式JDK 8 开始后的一种新的语法格式

Lambda 表达式 的标准格式

() -> { 

}
  • ():对应着方法的形参
  • ->:固定格式
  • { }:对应着方法的方法体

注意

  • Lambda 表达式 可以用来简化匿名内部类的书写
  • Lambda 表达式 只能用来简化函数式接口匿名内部类的写法。
    • 函数式接口有且仅有一个抽象方法的接口叫做函数式接口,接口上方可以加 @FunctionalInterface 注解。
    • @FunctionalInterface 注解:该注解只能标记在 “有且仅有一个抽象方法” 的接口上,主要用于编译期的错误检查。
    • 匿名内部类 是一种特殊的局部内部类,其本质是一个继承了该类或者实现了该接口的子类匿名对象。
    • 局部内部类 是在方法中定义的类,所以外界是无法直接调用的,需要在方法内部创建对象并使用。
    // 匿名内部类书写格式
    
    方法名(new 类名/接口名() {
    	重写的方法
    });
    

1.2.示例


1.2.1.示例一


这里以工具类 Arrays 为例。(public static void sort(数组, 排序规则) 是它的成员方法之一)

Arrays.sort(data, new Comparator<Integer>() {
    @Override
    public int compare(Integer t1, Integer t2) {
        return t1 - t2; 
    }
});

可以简化上方代码块中的代码

Arrays.sort(data, (o1, o2) -> {
    return o2 - o1;
});

1.2.2.示例二


interface Swim {
    public abstract void swimming();
}
public class ZDemo_1 {
    public static void main(String[] args) {
        testAnonymousInnerClass();
        testLambda();
    }

	// 利用匿名内部类的形式去调用函数式接口中的方法
    private static void testAnonymousInnerClass() {
        swimMethod(new Swim() {
            @Override
            public void swimming() {
                System.out.println("[匿名内部类]正在游泳 ... ...");
            }
        });
    }

	// 利用 Lambda 表达式的形式去调用函数式接口中的方法
    private static void testLambda() {
        swimMethod(() -> System.out.println("[Lambda表达式]正在游泳"));
    }

    public static void swimMethod(Swim swimClass) {
        swimClass.swimming();
    }
}

1.3.总结


  • Lambda 表达式 的基本作用

简化函数式接口的匿名内部类的写法。


  • Lambda 表达式 的使用前提

必须是接口的匿名内部类,且接口中只能有一个方法。


  • Lambda 表达式 的好处

Lambda 是一个匿名函数,我们可以把 Lambda 表达式 理解为一段可以传递的代码,可以用它写出更加简洁和灵活的代码。

Lambda 表达式 作为一种更加紧凑的代码风格,使得 Java 语言的表达能力得到了提升。


1.4.Lambda 表达式的省略规则


  • 参数类型可以省略不写
  • 如果接口中的抽象方法只有一个参数,参数类型是可以省略的,同时 () 也可以省略
  • 如果 Lambda 表达式 的方法体只有一行,{};return 都可以省略不写(这些需要同时省略)。

  • 示例
Integer[] integers = {1, 2, 3, 6, 8, 9, 7, 5, 4};
Arrays.sort(integers, new Comparator<Integer>() {
    @Override
    public int compare(Integer unsorted, Integer sorted) {
        return unsorted - sorted;
    }
});

Arrays.sort(integers, (Integer unsorted, Integer sorted) -> {
    return unsorted - sorted;
});

Arrays.sort(integers, (unsorted, sorted) -> {
    return unsorted - sorted;
});

Arrays.sort(integers, (unsorted, sorted) -> unsorted - sorted);

1.5.案例练习


定义数组,并存储字符串,利用 Arrays 中的 sort() 方法进行排序

要求:要按照字符串的长度进行排序,短的在前,长的在后

public class ZDemo_4 {
    public static void main(String[] args) {
        String[] strings = {"A", "AAAA", "A", "AAA"};
        test(strings);
        System.out.println(Arrays.toString(strings));
    }

    private static void test(String[] strings) { ... }
}

test() 方法中的内容:

  • 匿名内部类 的写法(升序排序)
Arrays.sort(strings, new Comparator<String>() {
    @Override
    public int compare(String unsorted, String sorted) {
        return unsorted.length() - sorted.length();
    }
});
[A, A, AAA, AAAA]
  • Lambda 表达式 的写法(降序排序)
Arrays.sort(strings, (unsorted, sorted) -> sorted.length() - unsorted.length());
[AAAA, AAA, A, A]

补充二:泛型


2.1.基本介绍


泛型是 JDK 5 中引入的特性,它提供了编译时类型安全检测机制。


2.1.1.泛型统一了数据类型


在没有泛型的时候,集合是如何创建数据的呢?

如果我们没有给集合指定类型,那么会默认所有数据都是 Object 类型,此时可以往集合中添加任意的数据类型。
但是这样一来有一个坏处:我们在获取数据的时候,无法使用它的特有行为。

ArrayList list = new ArrayList();
Collections.addAll(list, 123, "测试", new Student("张三", 33));

Iterator iterator = list.iterator();
while (iterator.hasNext()) {
    Object object = iterator.next();
    System.out.println(object);
}

如果我们要在上述的遍历操作中强制类型的话,会出现类型转换错误。
报错:Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String ...

String string = (String) iterator.next(); 

此时我们就可以使用泛型来处理这些问题。
我们可以在添加数据的时候就把集合中的类型统一了,而且我们在获取数据的时候,也无需强转了,非常方便。

ArrayList<String> list2 = new ArrayList<>();
Collections.addAll(list2, "曹彬", "李继隆", "郭守文");

2.1.2.泛型中的注意事项


注意事项

  • Java 中的泛型是伪泛型,在 .java 文件编译为 .class 文件后,所有的泛型信息都会被擦除。
    此即为 泛型擦除 现象。泛型本质上是编译器行为。
  • 泛型中不能写基本数据类型
  • 指定泛型的具体类型后,传递数据时,可以插入该类类型或者其子类类型
  • 如果集合不写泛型,类型默认是 Object
// 这里的方法重载,会报错。
private void test11(List<String> list){}
private void test11(List<Integer> list){}

Stream 流 【学习笔记】Java 基础_第2张图片


2.1.3.小结


泛型的介绍

  • 泛型是 JDK 5 中引入的特性,它提供了编译时类型安全检测机制。

泛型的好处

  • 统一了数据类型,把运行时期会遇到的问题提前到了编译期间,避免了强制类型转换可能出现的异常,因此在编译阶段就可以确定类型。(多态的弊端就是不能访问子类的特有功能。编译器可以对泛型参数进行检测,通过泛型参数可以指定传入的对象的类型)

泛型的定义格式

  • <类型>:指定一种类型的格式。尖括号里面可以任意书写,一般只写一个字母。例如:
  • <类型1, 类型2 …>:指定多种类型的格式,多种类型之间用逗号隔开。例如:

2.2.泛型类


使用场景:当一个类中,某个变量的数据类型不确定的时候,就可以定义带有泛型的类。

修饰符 class 类名<类型> {

}

例如:public class MyArrayList { }

创建该类的对象的时候,E 就要确定类型了:MyArrayList myList = new MyArrayList<>();

此处的 E 可以理解为变量,但是不是用来记录数据的,而是用来记录数据的类型的。(可以写成 TEKV 等)


2.3.泛型方法


方法中的形参不确定的时候

  • 可以使用类名后面定义的泛型 ,这个泛型是所有方法都可以用的
  • 在该方法的申明上定义自己的泛型,这个泛型只有该方法可以用
修饰符 <类型> 返回值类型 方法名(类型 变量名) {

}

例如:public static void addAll(ArrayList list, E... e),在调用该方法的时候,E 就要确定类型了

public static <E> void addAll(ArrayList<E> list, E... e) {
    for (E element : e) {
        list.add(element);
    }
}
public static void main(String[] args) {
    ArrayList<String> list1 = new ArrayList<>();
    addAll(list1, "薛仁贵", "苏定方", "李治", "武则天");
    System.out.println(list1);
}

此处的 E 可以理解为变量,但是不是用来记录数据的,而是用来记录数据的类型的。(可以写成 TEKV 等)


2.4.泛型接口


修饰符 interface 接口名<类型> {

}

如何使用一个带泛型的接口?

  • 实现类给出具体的类型
    举例:public class MyArrayList implements List { }
  • 实现类延续类型,创建对象的时候再确立
    举例:public class MyArrayList implements List { }

2.5.泛型的继承和通配符


泛型不具备继承性,但是数据是具备继承性的。

此时我们就可以使用泛型的通配符

  • ? 表示不确定的类型
  • 上界通配符: 该通配符为 ClassType 的所有子类型。它表示可以传递 ClassType 类型的类或其子类。
  • 下界通配符: 该通配符为 ClassType 的所有超类型。它表示可以传递 ClassType 类型的类或其子类。

应用场景

  • 我们在定义类、方法、接口的时候,如果类型不确定,就可以定义泛型类、泛型方法、泛型接口
  • 如果类型不确定,但是知道以后只能传递某个继承体系中的类型的参数的时候,就可以使用泛型的通配符

泛型的通配符的关键点就在于其可以限定类型的范围。


public class GenericsDemo_5 {
    public static void main(String[] args) {
        List<GrandFather> list_1 = new ArrayList<>();
        List<Father> list_2 = new ArrayList<>();
        List<Son> list_3 = new ArrayList<>();

        method_1(list_1);
        method_1(list_2);
        method_1(list_3);

        method_1(list_1);
        method_2(list_2);
        // method_2(list_3); // 报错
    }

	// 可以传递 GrandFather 类,以及 GrandFather 的子类
    public static void method_1(List<? extends GrandFather> list) { }

	// 该方法可以传递 Father 类,以及 Father 的父类
    private static void method_2(List<? super Father> list) { }
}

class GrandFather { }
class Father extends GrandFather { }
class Son extends Father { }

2.6.总结


泛型的概念

  • 泛型是 JDK 5 中引入的特性,可以在编译阶段约束操作的数据类型,并对其进行检查。

泛型的好处

  • 统一了集合中的数据类型
  • 把运行时期会遇到的问题提前到了编译期间,避免了强制类型转换可能出现的异常,因此在编译阶段类型就能确定下来。

泛型的定义格式

  • <类型>:指定一种类型的格式。尖括号里面可以任意书写,一般只写一个字母。例如:
  • <类型1, 类型2 …>:指定多种类型的格式,多种类型之间用逗号隔开。例如:

注意事项

  • 泛型中不能写基本数据类型
  • 指定泛型的具体类型后,传递数据时,可以插入该类类型或者其子类类型
  • 如果集合不写泛型,类型默认是 Object

定义泛型的位置

  • 泛型类:在类名后面定义泛型,创建该类对象的时候,需要指定类型
  • 泛型方法:在修饰符后面定义方法,调用该方法的时候,需要确定类型
  • 泛型接口:在接口名后面定义泛型,实现类确定类型,或实现类延续泛型

泛型的继承和通配符

  • 泛型不具备继承性,但数据具备继承性
  • 泛型的通配符:?(不确定的类型)、

使用场景

  • 定义类、方法、接口时,如果类型不确定,就可以定义泛型。
  • 如果类型不确定,但是能知道是哪个体系中的,就可以是使用泛型的通配符。

补充三:方法引用


3.1.体验方法引用


3.3.1.方法引用的出现原因


在使用 Lambda 表达式的时候,我们实际上传递进去的代码就是一种解决方案:拿参数做操作。

那么考虑一种情况:如果我们在 Lambda 中所指定的操作方案,已经有地方存在相同方案,那是否还有必要再写重复逻辑呢?
答案肯定是没有必要的。

那我们又是如何使用已经存在的方案的呢?

这就是我们要讲解的方法引用,我们是通过方法引用来使用已经存在的方案


3.3.2.基本概念


  • 什么是方法引用?
    • 把已经存在的方法拿过来用,当做函数式接口中的抽象方法的方法体
  • 方法引用符 ::
    • 该符号为引用运算符,而它所在的表达式被称为方法引用
  • 推导与省略
    • 如果使用 Lambda,那么根据 “可推导就是可省略” 的原则,无需指定参数类型,也无需指定的重载形式,它们都将被自动推导
    • 如果使用方法引用,也是同样可以根据上下文进行推导
    • 方法引用是 Lambda 的孪生兄弟
  • 方法引用时需要注意的细节
    • 一定要是函数式接口
    • 被引用的方法必须已经存在(这个被引用的方法可以是 JAVA 已经写好的,也可以是一些第三方的工具类)
    • 被引用方法的形参和返回值需要和抽象方法的形参和返回值保持一致
    • 被引用方法的功能要满足当前需求

3.3.3.示例代码


public class FunctionDemo1 {
    public static void main(String[] args) {
        Integer[] array = {3, 5, 4, 1, 6, 2};
        // anonymousInnerClassTest(array);
        // lambdaRepresentationTest(array);
        methodReferenceTest(array);
        System.out.println(Arrays.toString(array)); // [6, 5, 4, 3, 2, 1]
    }

    // Lambda表达式的方式
    private static void lambdaRepresentationTest(Integer[] array) {
        Arrays.sort(array, (unsorted, sorted) -> sorted - unsorted);
    }
    
    // 匿名内部类的方式
    private static void anonymousInnerClassTest(Integer[] array) {
        Arrays.sort(array, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        });
    }

    // 方法引用的方式
    private static void methodReferenceTest(Integer[] array) {
        Arrays.sort(array, FunctionDemo1::subtraction);
    }
    public static int subtraction(int num1, int num2) {
        return num2 - num1;
    }
}

3.2.引用静态方法


  • 格式类名::静态方法
  • 范例Integer::paresInt

案例需求:将 List 集合中存入的字符串全部转换为 Integer 类型。

public class FunctionDemo2 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        Collections.addAll(list, "1", "2", "3", "4", "5", "6");
        // anonymousInnerClassType(list);
        // lambdaType(list);
        methodReferenceType(list);
    }

    // 匿名内部类的方式
    private static void anonymousInnerClassType(List<String> list) {
        list.stream().map(new Function<String, Integer>() {
            @Override
            public Integer apply(String s) {
                return Integer.parseInt(s);
            }
        }).forEach(System.out::print);
    }

    // Lambda 表达式的方式
    private static void lambdaType(List<String> list) {
        list.stream().map(s -> Integer.parseInt(s)).forEach(s -> System.out.print(s));
    }

    // 方法引用的方式
    private static void methodReferenceType(List<String> list) {
        list.stream().map(Integer::parseInt).forEach(System.out::print);
    }
}

3.3.引用成员方法


  • 格式对象::成员方法
  • 范例其他类):对象::方法名
    • 如果是静态方法则为 其他类的类名::方法名
    • 如果是普通成员方法则为 new 其他类::方法名其他类的实例对象::方法名
  • 范例本类):this::方法名
    • 注意事项this 关键字不能直接调用静态方法!
  • 范例父类):super::方法名

案例需求:按照要求过滤集合中的数据(只要 “张” 字开头的字符串,内容长度是三个字)

其他类:StringOperation.java

public class StringOperation {
    public boolean stringJudge(String s) {
        return s.startsWith("张") && s.length() == 3;
    }
}

FunctionDemo3.java

public class FunctionDemo3 {
    public static void main(String[] args) {
        List<String> nameList = new ArrayList<>();
        Collections.addAll(nameList, "张无忌", "周芷若", "赵敏", "张翠山", "张三丰", "张强");
        // lambda(nameList);
        // anonymousClass(nameList);
        methodReference(nameList);
    }

    // Lambda 表达式的方式
    private static void lambda(List<String> list) {
        list.stream()
                .filter(s -> s.startsWith("张")).filter(s -> s.length() == 3)
                .forEach(s -> System.out.print(s + " "));
    }

    // 匿名内部类的方式
    private static void anonymousClass(List<String> list) {
        list.stream()
                .filter(new Predicate<String>() {
                    @Override
                    public boolean test(String s) {
                        return s.startsWith("张") && s.length() == 3;
                    }
                }).forEach(s -> System.out.print(s + " "));
    }

    // 方法引用
    private static void methodReference(List<String> list) {
        StringOperation so = new StringOperation();
        list.stream().filter(so::stringJudge).forEach(s -> System.out.print(s + " "));
    }
}

如果诸位对 “this 关键字不能直接调用静态方法” 这句话有疑问的话,可以看看下面的视频解惑一二。

  • this 的内存原理https://www.bilibili.com/video/BV17F411T7Ao/?p=89
  • 成员变量和局部变量的区别https://www.bilibili.com/video/BV17F411T7Ao?p=90
  • 静态变量的修饰符 statichttps://www.bilibili.com/video/BV17F411T7Ao?p=121
  • static 关键字的注意事项https://www.bilibili.com/video/BV17F411T7Ao?p=123

静态方法不是属于实例对象的,它是属于类的,一旦类被加载它的字节码信息就会加载到方法区中。

this 关键字是记录调用者对象(其实就是实例对象)的地址,即哪个对象调用它,它就引用哪个对象。

静态方法不能通过实例对象来调用,要类才可以调用它,那 this 自然也不可以调用静态方法了。

这里再说一些题外话,JDK 7 之后类的字节码信息就开始从方法区的永久代逐步迁移到元空间中了,堆中已经有一块专门的区域来记录静态变量中了。方法区在 JDK 8 之后本来就已经是一个逻辑概念了,并没有严格意义上的物理区域。


稍稍总结(精简)一下上面的四个视频中的内容

区别 成员变量 局部变量
类中的位置不同 类中、方法外的变量 方法内、方法申明上的变量
初始化值不同 有默认的初始化值 没有默认的初始值,使用之前需要完成赋值操作
内存位置不同 堆内存 栈内存
生命周期不同 随着对象的创建而存在,随着对象的消失而消失 随着方法的调用而存在,随着方法运行的结束而消失
作用域 在整个类中都有效 只在在当前方法中有效
  • this 关键字
    • 作用:区分成员变量和局部变量
    • 本质:记录所在方法的调用者的地址值
  • static 关键字
    • static 修饰的成员变量,叫静态变量
      • 特点①:被该类所有对象共享
      • 特点②:与对象无关。其随着类的加载而加载,优先于对象存在。
      • 调用方式:类名调用(推荐)、对象名调用
    • static 修饰的成员方法,叫做静态方法
      • 应用场景:大多用在测试类和工具类中,单例模式中也会用到它,JavaBean 中很少会用到它。
      • 调用方式:类名调用(推荐)、对象名调用
    • 注意事项
      • 静态方法只能访问静态变量和静态方法
      • 非静态方法可以访问静态变量或静态方法,也可以访问非静态的成员变量和非静态的成员方法
      • 静态方法中不可以存在 this 关键字

这个内容也是上面的四个视频里的,不过蛮有意思的,就抄下来了。

重新认识 main 方法

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}
  • public:被 JVM 调用,访问权限足够大
  • static:被 JVM 调用,不用创建对象,直接可以通过类名访问
    • 因为 main 方法是静态的,所以测试类中其他方法也是需要是静态的。
  • void:被 JVM 调用,不需要给 JVM 返回值
  • main:一个通用的名称,虽然不是关键字,但是被 JVM 识别。
  • String[] args:以前用来接收键盘录入数据的,现在已经没有多大用处了。

3.4.引用构造方法


  • 格式类名::new
  • 范例Student::new

案例需求:将集合中存储的数据,封装到 Stuent 对象中,并收集到 List 集合中。

  • Student.java
public class Student {
    private String name;
    private int age;

    public Student(String s) {
       this.name = s.split(",")[0];
       this.age = Integer.parseInt(s.split(",")[1]);
    }
	
	// 其他的构造方法、get/set方法、toString 方法略
}
  • FunctionDemo4.java
public class FunctionDemo4 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        Collections.addAll(list,
                "张无忌,15", "周芷若,14", "赵敏,13", "张强,20", "张三丰,108",
                "张翠山,40", "张良,35", "王二麻子,37", "谢广坤,41");

        // List newList = anonymousInnerClass(list);
        // List newList = lambda(list);
        List<Student> newList = methodReference(list);

        newList.forEach(System.out::println);
    }

    // 匿名内部类的方式
    private static List<Student> anonymousInnerClass(List<String> list) {
        return list.stream().map(new Function<String, Student>() {
            @Override
            public Student apply(String s) {
                String name = s.split(",")[0];
                int age = Integer.parseInt(s.split(",")[1]);
                return new Student(name, age);
            }
        }).collect(Collectors.toList());
    }

    // Lambda 表达式的方式
    private static List<Student> lambda(List<String> list) {
        return list.stream()
                .map(s -> new Student(s.split(",")[0], Integer.parseInt(s.split(",")[1])))
                .collect(Collectors.toList());
    }

    // 方法引用的方式
    private static List<Student> methodReference(List<String> list) {
        return list.stream().map(Student::new).collect(Collectors.toList());
    }

}

3.5.使用类名引用成员方法


  • 格式类名::成员方法
  • 范例String::substring

案例需求:集合里有一些字符串,要求全部变成大写后再输出

public class FunctionDemo5 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        Collections.addAll(list, "aaa", "bbb", "ccc", "ddd", "eee");
        // anonymousInnerClass(list);
        methodReference(list);
    }

	// 匿名内部类的方式
    private static void anonymousInnerClass(List<String> list) {
        list.stream().map(new Function<String, String>() {
            @Override
            public String apply(String s) {
                return s.toUpperCase();
            }
        }).forEach(s -> System.out.print(s + " "));
    }

	// 方法引用的方式
    private static void methodReference(List<String> list) {
        list.stream().map(String::toUpperCase).forEach(s -> System.out.print(s + " "));
    }
}

  • java/lang/String.java
public String toUpperCase() {
    return toUpperCase(Locale.getDefault());
}

显然,public String toUpperCase()public String apply(String s) 的形参是不一样的。


  • 使用 类名::成员方法 有着自己的规则
    • 需要有函数式接口
    • 被引用的方法必须是已经存在的
    • 被引用方法的形参,需要更抽象方法的第二个形参到最后一个形参保持一致,返回值需要保持一致
    • 被引用方法的功能需要满足当前的需求
  • 抽象方法形参详细介绍
    • 第一个参数:表示被引用方法的调用者,觉得了可以用于哪些类中的方法。
      • Stream 流中,第一个参数一般都表示流里面的每一个数据。
      • 假设流里面的数据是字符串,那么使用这种方式进行方法引用,则只能引用 String 这个类中的方法。
    • 第二个参数到最后一个参数:与被引用方法的形参保持一致。
      • 如果没有第二个参数,说呢被引用的方法需要的是无参的成员方法。
    • 局限性
      • 不能引用所有类中的成员方法。
      • 抽象方法中的第一个参数是什么类型的,就只能引用这个类中的方法。

故可以这样理解下面这段代码

  • 拿着流里面的每一个数据,去调用 String 类中的 toUpperCase() 方法,方法的返回值就是转换后的结果。
list.stream().map(String::toUpperCase).forEach(s -> System.out.print(s + " "));

3.6.引用数组的构造方法


  • 格式数据类型[]::new
  • 范例Integer[]::new
  • 目的创建一个指定类型的数组
  • 细节数组的类型,需要和流中的数据类型保持一致

需求:集合在存储一些整数,收集到数组中去

public class FunctionDemo6 {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        Collections.addAll(list, 1, 2, 3, 4, 5, 6, 7, 8);
        // Integer[] array = anonymousInnerClass(list);
        Integer[] array = methodReference(list);
        System.out.print(Arrays.toString(array));
    }

    // 匿名内部类的方式
    private static Integer[] anonymousInnerClass(List<Integer> list) {
        return list.stream().toArray(new IntFunction<Integer[]>() {
            @Override
            public Integer[] apply(int value) {
                return new Integer[value];
            }
        });
    }

    // 方法引用的方式
    private static Integer[] methodReference(List<Integer> list) {
        return list.stream().toArray(Integer[]::new);
    }
}

3.7.总结


  • 什么是方法引用
    • 直接把已经存在的方法拿过来用,当做函数式接口中的方法体。
  • :: 符号是方法引用符
  • 方法引用的时候需要注意什么
    • 需要有函数式接口
    • 被引用方法必须是已经存在的
    • 被引用方法的形参和返回值需要和抽象方法的形参和返回值保持一致
    • 被引用方法的功能要满足当前的需求

  • 引用静态方法类名::静态方法
  • 引用成员方法
    • 对象::成员方法
    • this::成员方法
    • super::成员方法
  • 引用构造方法
    • 类名::new
  • 使用类名引用成员方法
    • 类名::成员方法
    • 注意事项
      • 不能引用所有类中的成员方法。如果抽象方法的第一个参数是 A 类型的,那么只能引用 A 类中的方法。
  • 引用数组的构造方法
    • 数组类型[]::new

你可能感兴趣的:(Java,基础学习,#,java,java,学习)