java8新特性Stream的基本使用

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_33643072/article/details/81221635

 第一、创建Stream

     1.1、根据集合的创建

        List<String> list = new ArrayList<>();
        Collections.addAll(list,"aa","aaa","Aaaa","b");//这段代码相当于使用了4次list.add
        Stream<String> stream = list.stream();

     1.2、根据数组创建

        //String数组
        String[] strings = {"aa","aaa","Aaaa","b"};
        Stream<String> stringStream=Arrays.stream(strings);//第一种
        Stream<String> stringStream1=Stream.of(strings);//第二种
        //Integer数组
        Integer[] integers = {1,2,3,4,5,6};
        Stream<Integer> integerStream=Arrays.stream(integers);
        Stream<Integer> integerStream1=Stream.of(integers);//第二种

(下面两种创建方式不理解的话,先看完流操作,再回来看看) 

1.3、根据文件创建

    public static void main(String[] args) throws FileNotFoundException {
        AtomicLong lineCount = new AtomicLong(0);//计算文件的行数
        List<String> stringList = new ArrayList<>();//用于接收读取的每一行内容,当size为一万时,存入数据库,然后清空

        File file = new File(ResourceUtils.getURL("classpath:"+"test.txt").getPath());//我这是SpringBoot项目,此方法是获取resources目录下的test.txt,如果test.txt在resources\static下可以写成"classpath:"+"static/test.txt"
//        File file = new File("F:"+File.separator+"test.txt");//也可以这样写,文件存在F:\test.txt
        if (!file.exists()){
            System.out.println("文件不存在!!");
            return;
        }
            
        System.out.println(file.getPath());
        try(BufferedReader reader = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) {
            Stream<String> stringStream=reader.lines();//得到流
            stringStream.forEach(line -> {
                System.out.println(line);
                stringList.add(line);
                lineCount.addAndGet(1);

                if (lineCount.longValue()%10000==0){
                    //当stringList里有一万条数据时存一次数据库,然后清空stringList,这对于大小上G的文件读取很有用
                }
            });
        }catch (IOException e) {
            e.printStackTrace();
        }

    }

 1.4、根据函数创建无限流

   Stream API提供了两个静态方法创建无限流:Stream.iterate和Stream.generate,这样流的元素是无穷无尽的,一般用limit加以控制

         1.4.1 Stream.iterate

Stream.iterate(0,n->n+2).limit(10).forEach(System.out::println);//将会从0开始打印十个双数

         1.4.2 Stream.generate

Stream.generate(Math::random).limit(10).forEach(System.out::println);//输出十个0-1的随机双精度数

第二、流操作

      流操作分为:中间操作和终端操作

      中间操作会返回另外一个流,而终端操作则会返回一个非流的结果如Integer、List、void(根据返回结果很容易区分是中间操作还是终端操作了)

      因为中间操作返回的是一个流,所以还可以调用其他的中间操作,看起来就像是一条流水线,直到执行终端操作

      利用代码来看一下,filter和map是中间操作,collect是终端操作

        List<String> list = new ArrayList<>();
        Collections.addAll(list,"aa","aaa","Aaaa","b");
        List<String> list1=list.stream().filter(s -> s.contains("a")).map(s -> s.toLowerCase()).collect(toList());
        //上面这段代码就像是:过滤出只包含“a”的内容->将上一次得到的内容转成小写->返回一个List的结果
        //list1得到的数据是:[aa, aaa, aaaa]

   下面我们会一一介绍中间操作和终端操作,并且都会有一个简单的使用的例子

   2.1 filter(中间操作)

//我们先看一下filter方法接收什么、下面是java.util.stream包下的Stream接口中的方法,这个filter到底是具体怎么实现的我们不需要管,我只需要知道这个方法接收什么类型的参数和返回时什么类型的参数。
//这里接收的是一个函数式接口Predicate(函数式接口:接口中有且只有一个抽象方法,他就是函数式接口,但是他可以有多个默认方法和静态方法),返回的是Stream返回的还是流所以filter是一个中间操作。
Stream<T> filter(Predicate<? super T> predicate);

Stream涉及到许多函数式接口,所以我建议在学习Stream之前先学习一下java8的另外一个新特性Lambda.

Predicate中的抽象方法test接收的是泛型T,返回一个boolean值。

所以filter操作会接受一个返回boolean的函数作为参数,并且返回一个包含所有符合条件(函数返回true)的新流。

        List<String> list = new ArrayList<>();
        Collections.addAll(list,"aa","aaa","Aaaa","b");
        List<String> list1=list.stream().filter(s -> s.length()>2).collect(toList());

这里我们从list中筛选出长度大于2的,符合条件的是“aaa”和“Aaaa”.

2.2 distinct(中间操作)

用于去除重复的内容

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        Collections.addAll(list,"aaa","aaa","Aaaa","b");//有两个相同的“aaa”
        List<String> list1=list.stream().filter(s -> s.length()>2).distinct().collect(toList());//配合filter使用
        System.out.println(list1);//打印[aaa, Aaaa],//去除重复的“aaa”,只保留一个
    }

2.3 limit(中间操作)

返回一个不超过给定长度的流。如果流是有序的如Stream<List>会返回前n个元素的新流,也可用于无序如Set.

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        Collections.addAll(list,"aaa","aaa","Aaaa","b");
        List<String> list1=list.stream().limit(3).collect(toList());//只取3个元素,去掉后面的
        System.out.println(list1);//[aaa, aaa, Aaaa]
    }

2.4 skip(中间操作)

跳过给定的n个元素,下如跳过n=2个元素,如果n>=list.size这会返回一个空流

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        Collections.addAll(list,"aaa","aaa","Aaaa","b");
        List<String> list1=list.stream().skip(2).collect(toList());//跳过前面2个元素,只取后面的
        System.out.println(list1);//[Aaaa, b]
    }

2.5 map(中间操作)

map会接收一个函数作为参数,如下代码中s.leng()返回Integer,所以map返回的新流是Stream<Integer>,然后我们toList()将其转化为集合List<Integer>

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        Collections.addAll(list,"aaa","aaa","Aaaa","b");
        List<Integer> list1=list.stream().map(s -> s.length()).collect(toList());//配合filter使用
        System.out.println(list1);//[3, 3, 4, 1]
    }

 我们也可以用于处理对象的集合,从中取出一个属性作为集合,相当mysql中从一个表里取出中一列数据

public class User {
    private String name;
    private int age;
    public User() {
    }

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
//get、set、toString...
}

 下面我们取出所有的姓名name,因为user.getName()返回的是String所有map返回的新流是Stream<String>,toList()之后返回的是List<String>

    public static void main(String[] args) {
        List<User> list2 = new ArrayList<>();
        Collections.addAll(list2,new User("张三",25),new User("李四",26),new User("王五",28));
        List<String> list3 = list2.stream().map(user -> user.getName()).collect(toList());
        System.out.println(list3);//[张三, 李四, 王五]
    }

2.6 flatMap(中间操作)

将流中的每个值都换成另一个流,然后把所有流连接成一个流。

    public static void main(String[] args) {
        List<String> list4 = new ArrayList<>();
        Collections.addAll(list4,"hello","world");
        List<String> stream=list4.stream().map(s -> s.split("")).flatMap(strings -> Arrays.stream(strings)).distinct().collect(toList());
        System.out.println(stream);//[h, e, l, o, w, r, d]
    }

2.7 anyMatch(终端操作)

anyMatch就像是:流中至少有一个元素能匹配,anyMatch返回的是一个boolean值,所以不是中间操作而是终端操作

    public static void main(String[] args) {
        List<User> list2 = new ArrayList<>();
        Collections.addAll(list2,new User("张三",25),new User("李四",26),new User("王五",28));
        if(list2.stream().anyMatch(user -> user.getName().equals("李四"))){
            System.out.println("这个集合中是有一个叫做李四的人");//将会打印出
        }
    }

2.8 allMatch(终端操作)

allMatch就像是:流中是否所有元素都匹配

    public static void main(String[] args) {
        List<User> list2 = new ArrayList<>();
        Collections.addAll(list2,new User("张三",25),new User("李四",26),new User("王五",28));
        if(list2.stream().allMatch(user -> user.getAge()==25)){
            System.out.println("这个集合中的人都是25岁");//不会打印,需要所以的年龄都是25,可以自行更改验证
        }
    }

2.9 noneMatch(终端操作)

noneMatch就像是:流中是否所有元素都不匹配。noneMatch和allMatch是相对的。noneMatch是所有都不匹配才是true,而allMatch所有都匹配才是true

    public static void main(String[] args) {
        List<User> list2 = new ArrayList<>();
        Collections.addAll(list2,new User("张三",25),new User("李四",26),new User("王五",28));
        if(list2.stream().noneMatch(user -> user.getName().equals("赵六"))){
            System.out.println("将会打印输出,因为集合中没有叫做赵六的");//所有都不匹配,打印
        }
    }

2.10 findAny(终端操作)

获取任一元素,返回流中任意一个元素,返回的是Optional而不是流,所以也是终端操作。Optional是java8新引入的容器类,代表一个值存在或者不存在,用来处理值为null的问题。(注意findAny返回的只有一个元素,和我们下面将要将的findFirst一样只返回一个)

    public static void main(String[] args) {
        List<User> list2 = new ArrayList<>();
        Collections.addAll(list2,new User("张三",25),new User("李四",26),new User("王五",28));
        Optional<User> optional= list2.stream().filter(user -> user.getAge()<=30).findAny();
        if (optional.isPresent()){
            System.out.println("optional有值我将会打印输出");
        }
        optional.ifPresent(user -> System.out.println(user.toString()));
        User u = optional.get();
        User u2 = optional.orElse(new User("赵六",30));
    }

Optional操作也很简单,有几个比较常用的方法:

boolean isPresent():将在Optional包含值的时候返回true,否则返回false

void ifPresent(Consumer<? super T> consumer):会在值存在的时候执行给定的代码块

T get():会在值存在时返回值,否则抛出NoSuchElementException异常

T orElse(T other):值存在时返回值,否则返回默认值other

2.11 findFirst(终端操作)

findFirst与findAny类似也是返回一个元素Optional,findFirst用于有序的流中如Stream<List>等

    public static void main(String[] args) {
        List<User> list2 = new ArrayList<>();
        Collections.addAll(list2,new User("张三",25),new User("李四",26),new User("王五",28));
        Optional<User> optional= list2.stream().filter(user -> user.getAge()<=30).findFirst();
    }

2.12 reduce(终端操作)

reduce多用于处理数值,如求最大值、最小值、求和等

如下求和,有六种方法其结果都是一样的。

    public static void main(String[] args) {
        List<Integer> list3 = new ArrayList<>();
        Collections.addAll(list3,4,7,5,6);
        int sum = list3.stream().reduce(0,(a,b)->a+b);
        int sum1 = list3.stream().reduce(0,(a,b)->Integer.sum(a,b));
        int sum2 = list3.stream().reduce(0,Integer::sum);
        Optional<Integer> optional = list3.stream().reduce((a,b)->a+b);
        Optional<Integer> optional1 = list3.stream().reduce((a,b)->Integer.sum(a,b));
        Optional<Integer> optional2 = list3.stream().reduce(Integer::sum);

        System.out.println("总和为:"+sum);//总和为:22
        System.out.println("总和为:"+sum1);//总和为:22
        System.out.println("总和为:"+sum2);//总和为:22
        optional.ifPresent(sum3->System.out.println("总和为:"+sum3));
        optional1.ifPresent(sum4->System.out.println("总和为:"+sum4));
        optional2.ifPresent(sum5->System.out.println("总和为:"+sum5));
    }

理解了上面的六种方法之后,求最大值、最小值方法类似的如:求最大值(记得将reduce方法的第一个参数设置为流中包含的元素)

        int max = list3.stream().reduce(4,(a,b)->a>=b?a:b);
        int max1 = list3.stream().reduce(4,(a,b)->Integer.max(a,b));
        int max2 = list3.stream().reduce(4,Integer::max);
        System.out.println("最大值:"+max);
        System.out.println("最大值:"+max1);
        System.out.println("最大值:"+max2);

当然也可以设置得到的结果是Optional容器类(这个还不需要考虑如何填写第一个参数)

        Optional<Integer> optional3 = list3.stream().reduce((a,b)->a>=b?a:b);
        Optional<Integer> optional4 = list3.stream().reduce((a,b)->Integer.max(a,b));
        Optional<Integer> optional5 = list3.stream().reduce(Integer::max);

2.13 sorted(中间操作)

sorted处理List<String>

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        Collections.addAll(list,"aaa","aaa","Aaaa","b");
        List<String> list1=list.stream().sorted(Comparator.comparing(String::length)).collect(toList());
        System.out.println(list1);//[b, aaa, aaa, Aaaa]
        List<String> list2=list.stream().sorted(Comparator.comparing(String::length).reversed()).collect(toList());
        System.out.println(list2);//[Aaaa, aaa, aaa, b]

sorted处理List<Integer>

    public static void main(String[] args) {
        List<Integer> list3 = new ArrayList<>();
        Collections.addAll(list3,4,7,5,6);

        List<Integer> integerList = list3.stream().sorted((a,b)->a.compareTo(b)).collect(toList());
        System.out.println(integerList);//[4, 5, 6, 7]
        List<Integer> integerList2 = list3.stream().sorted((a,b)->b.compareTo(a)).collect(toList());
        System.out.println(integerList2);//[7, 6, 5, 4]
    }

sorted处理List<User>

    public static void main(String[] args) {
        List<User> list4 = new ArrayList<>();
        Collections.addAll(list4,new User("张三",27),new User("李四",27),new User("王五",28),new User("赵六",25));

        List<User> userList=list4.stream().sorted(Comparator.comparing(User::getAge).thenComparing(User::getName)).collect(toList());//先按年龄从小到大排序,如果年龄相同者根据名字从小到大排序
        System.out.println(userList);//[User{name='赵六', age=25}, User{name='张三', age=27}, User{name='李四', age=27}, User{name='王五', age=28}]
        List<User> userList2=list4.stream().sorted(Comparator.comparing(User::getAge).reversed()).collect(toList());
        System.out.println(userList2);//[User{name='王五', age=28}, User{name='张三', age=27}, User{name='李四', age=27}, User{name='赵六', age=25}]
    }

2.14 count(终端操作)

计算元素的个数

        List<Integer> list3 = new ArrayList<>();
        Collections.addAll(list3,4,7,5,6);
        long count = list3.stream().count();//count=4

第三、收集器(收集器也属于流操作,不过内容太多,就分开写了)

 收集器之前我们也用过也就是collect(toList()),不过我们都是按顺序将流中的元素生成一个列表,收集器的功能可不是仅仅这样,下面将介绍更多。

3.1 counting统计

    public static void main(String[] args){
        List<User> list4 = new ArrayList<>();
        Collections.addAll(list4,new User("张三",27),new User("李四",27),new User("王五",28),new User("赵六",25));

        long count = list4.stream().collect(Collectors.counting());//统计
        System.out.println(count);//4
        long count2 = list4.stream().count();//两种方法结果一样
        System.out.println(count);//4
    }

3.2 maxBy、minBy  最大值和最小值

    public static void main(String[] args){  
        List<User> list4 = new ArrayList<>();
        Collections.addAll(list4,new User("张三",27),new User("李四",27),new User("王五",28),new User("赵六",25));

        Optional<User> optional = list4.stream().collect(Collectors.maxBy(Comparator.comparingInt(User::getAge)));//求年龄最大的
        optional.ifPresent(System.out::println);//User{name='王五', age=28}

        Optional<User> optional2 = list4.stream().collect(Collectors.minBy(Comparator.comparingInt(User::getAge)));//求年龄最小的
        optional2.ifPresent(System.out::println);//User{name='赵六', age=25}
    }

3.3 averaging平均值

    public static void main(String[] args){
        List<User> list4 = new ArrayList<>();
        Collections.addAll(list4,new User("张三",27),new User("李四",27),new User("王五",28),new User("赵六",25));

        Double ageAverage=list4.stream().collect(Collectors.averagingInt(User::getAge));//求年龄的平均值
        System.out.println(ageAverage);//26.75
    }

3.4  summing 求和

    public static void main(String[] args){
        List<User> list4 = new ArrayList<>();
        Collections.addAll(list4,new User("张三",27),new User("李四",27),new User("王五",28),new User("赵六",25));

        Integer sum=list4.stream().collect(Collectors.summingInt(User::getAge));
        System.out.println(sum);//107

    }

3.5 只需要一步就可以求出最大最小值、和、平均值、统计

    public static void main(String[] args){
        List<User> list4 = new ArrayList<>();
        Collections.addAll(list4,new User("张三",27),new User("李四",27),new User("王五",28),new User("赵六",25));

        IntSummaryStatistics intSummaryStatistics=list4.stream().collect(Collectors.summarizingInt(User::getAge));
        System.out.println(intSummaryStatistics);//IntSummaryStatistics{count=4, sum=107, min=25, average=26.750000, max=28}

    }

 3.6 joining 连接字符串

    public static void main(String[] args){
        List<User> list4 = new ArrayList<>();
        Collections.addAll(list4,new User("张三",27),new User("李四",27),new User("王五",28),new User("赵六",25));

        String joinName = list4.stream().map(User::getName).collect(Collectors.joining());//只提取名字进行连接
        System.out.println(joinName);//张三李四王五赵六
        String joinName2 = list4.stream().map(User::getName).collect(Collectors.joining(", "));//只提取名字进行连接,名字之间用", "分开
        System.out.println(joinName2);//张三, 李四, 王五, 赵六
        String joinName3 = list4.stream().map(User::getName).collect(Collectors.joining(", " , "[" , "]"));//只提取名字进行连接,名字之间用", "分开,并且加上前缀“[”和后缀“]”
        System.out.println(joinName3);//[张三, 李四, 王五, 赵六]


        String joinStr = list4.stream().map(User::toString).collect(Collectors.joining());//输出全部信息
        System.out.println(joinStr);//User{name='张三', age=27}User{name='李四', age=27}User{name='王五', age=28}User{name='赵六', age=25}
    }

3.7 groupingBy 分组

    public static void main(String[] args){
        Collections.addAll(list4,new User("张三",27),new User("李四",27),new User("王五",28),new User("赵六",25));

        Map<Integer,List<User>> map=list4.stream().collect(Collectors.groupingBy(User::getAge));//根据年龄分组
        System.out.println(map);//{25=[User{name='赵六', age=25}], 27=[User{name='张三', age=27}, User{name='李四', age=27}], 28=[User{name='王五', age=28}]}

        //上面是根据年龄的某一值分类,现在我们想要按范围分类,age<=25的则分类为"小鲜肉",25<age<28的分类"花样年华",28=<age的为"老腊肉"
        Map<String,List<User>> map2 = list4.stream().collect(Collectors.groupingBy(user->{
            int age = user.getAge();
            if (age<=25){
                return "小鲜肉";
            }else if (age>25&&age<28){
                return "花样年华";
            }else {
                return "老腊肉";
            }
        }));
        System.out.println(map2);//{老腊肉=[User{name='王五', age=28}], 小鲜肉=[User{name='赵六', age=25}], 花样年华=[User{name='张三', age=27}, User{name='李四', age=27}]}

    }

   多级分组

  为了更好的进行多级分组,我们在User类上加了个sex性别属性。

(注意看:第二个groupingBy是第一个groupingBy方法的参数,所以我们还可以在第二个groupingBy方法上继续加达到更多级的分组)

:先按照年龄再按照性别

    public static void main(String[] args){

        List<User> list4 = new ArrayList<>();
        Collections.addAll(list4,new User("张三",27,"男"),new User("李四",27,"女"),new User("王五",28,"男"),new User("赵六",25,"男"),new User("钱七",28,"男"));

        //先按照年龄范围分组,同一分组中再按照年龄分组
        Map<String, Map<String, List<User>>> map2 = list4.stream().collect(Collectors.groupingBy(user->{
            int age = user.getAge();
            if (age<=25){
                return "小鲜肉";
            }else if (age>25&&age<28){
                return "花样年华";
            }else {
                return "老腊肉";
            }
        },Collectors.groupingBy(User::getSex)));
        System.out.println(map2);//{老腊肉={男=[User{name='王五', age=28, sex='男'}, User{name='钱七', age=28, sex='男'}]}, 小鲜肉={男=[User{name='赵六', age=25, sex='男'}]}, 花样年华={女=[User{name='李四', age=27, sex='女'}], 男=[User{name='张三', age=27, sex='男'}]}}
    }

:先按照性别再按照年龄

    public static void main(String[] args){
        List<User> list4 = new ArrayList<>();
        Collections.addAll(list4,new User("张三",27,"男"),new User("李四",27,"女"),new User("王五",28,"男"),new User("赵六",25,"男"),new User("钱七",28,"男"));

        Map<String, Map<String, List<User>>> map2 = list4.stream().collect(Collectors.groupingBy(User::getSex,Collectors.groupingBy(user->{
            int age = user.getAge();
            if (age<=25){
                return "小鲜肉";
            }else if (age>25&&age<28){
                return "花样年华";
            }else {
                return "老腊肉";
            }
        })));
        System.out.println(map2);//{女={花样年华=[User{name='李四', age=27, sex='女'}]}, 男={老腊肉=[User{name='王五', age=28, sex='男'}, User{name='钱七', age=28, sex='男'}], 小鲜肉=[User{name='赵六', age=25, sex='男'}], 花样年华=[User{name='张三', age=27, sex='男'}]}}
    }

 :我们也可以对子组进行统计或其他操作

    public static void main(String[] args){
        List<User> list4 = new ArrayList<>();
        Collections.addAll(list4,new User("张三",27,"男"),new User("李四",27,"女"),new User("王五",28,"男"),new User("赵六",25,"男"),new User("钱七",28,"男"));

        Map<String, List<User>> map=list4.stream().collect(Collectors.groupingBy(User::getSex));//根据性别进行一级分组
        System.out.println(map);//{女=[User{name='李四', age=27, sex='女'}], 男=[User{name='张三', age=27, sex='男'}, User{name='王五', age=28, sex='男'}, User{name='赵六', age=25, sex='男'}, User{name='钱七', age=28, sex='男'}]}

        Map<String, Long> map2 = list4.stream().collect(Collectors.groupingBy(User::getSex,Collectors.counting()));
        System.out.println(map2);//{女=1, 男=4}
    }
    public static void main(String[] args){
        List<User> list4 = new ArrayList<>();
        Collections.addAll(list4,new User("张三",27,"男"),new User("李四",27,"女"),new User("王五",28,"男"),new User("赵六",25,"男"),new User("钱七",28,"男"));
        //先按照性别分组,再在子组中选出年龄最大的一个
        Map<String, User> map2 = list4.stream().collect(Collectors.groupingBy(User::getSex,collectingAndThen(maxBy(Comparator.comparingInt(User::getAge)),Optional::get)));
        System.out.println(map2);//{女=User{name='李四', age=27, sex='女'}, 男=User{name='王五', age=28, sex='男'}}
    }

 3.8 partitioningBy 分区

 为了更好的观察分组的功能,先在User类中增加下面一个方法isMan,如果是男的返回true,否则返回false.

    public boolean isMan(){
        return this.getSex().equals("男")?true:false;
    }

 partitioningBy分区其实和groupingBy分组几乎一样,区别在于partitioningBy方法第一个参数是一个返回boolean值的函数,得到的结果Map键是boolean,所以 partitioningBy分区只有两种结果,true或者false.

    public static void main(String[] args){
        List<User> list4 = new ArrayList<>();
        Collections.addAll(list4,new User("张三",27,"男"),new User("李四",27,"女"),new User("王五",28,"男"),new User("赵六",25,"男"),new User("钱七",28,"男"));

        Map<Boolean, List<User>> map=list4.stream().collect(Collectors.partitioningBy(User::isMan));
        System.out.println(map);//{false=[User{name='李四', age=27, sex='女'}], true=[User{name='张三', age=27, sex='男'}, User{name='王五', age=28, sex='男'}, User{name='赵六', age=25, sex='男'}, User{name='钱七', age=28, sex='男'}]}
        //分区和分组一样可以对子组进行一些其他操作
        Map<Boolean, Long> map2 = list4.stream().collect(Collectors.partitioningBy(User::isMan,counting()));
        System.out.println(map2);//{false=1, true=4}
    }

猜你喜欢

转载自blog.csdn.net/qq_33643072/article/details/81221635