day08【异常、lambda和Stream】

day08【异常、lambda和Stream】

今日内容

  • 异常
  • 异常处理
  • 多线程

教学目标

第一章 异常

1.1 异常概念

异常:不正常。

生活中的异常:

例:在上课时,突然间停电,造成上课终止。   处理:等待来电、使用备用发电机。

程序中的异常:

程序在运行的过程,出现了一些突发状况,造成程序无法继续运行。我们把上述的突发状况,无法正常运行的这些状态称为Java中的异常。

Java中的异常:就是程序中出现的错误(bug),或者不正常现象。而我们在开发程序的时候,就需要对这些问题进行预先的判断和处理。

学习Java中的异常,我们需要研究:

1、什么是异常;

2、异常问题怎么去解决和处理;

3、我们自己怎么把问题报告给程序的使用者;

我们编写程序的时候,在程序中肯定会有问题(bug)出现,而sun公司把开发中最最常见的一些问题,进行总结和抽取,形成了一个体系,这个体系就是我们要学习的异常体系。

异常指的并不是语法错误,语法错了,编译不通过,不会产生字节码文件,根本不能运行.

1.2 异常的产生过程解析(掌握)

需求:根据用户指定的下标,输出该下标对应的数据。

1)定义一个类ExceptionDemo;

2)定义一个主方法,在主方法中定义一个int类型的数组,在这个数组中存放int类型的数据;

3)在这个类中自定义一个函数getValue,在这个函数中根据调用者传递过来的下标和数组返回给调用者

对应的数据;

4)在主方法中调用自定义函数getValue,接收自定义函数返回回来的数据,并将数据打印到屏幕上;

说明:上述代码发生异常的过程:

1)jvm先去调用main函数,在main函数中调用了getValue函数,然后jvm将getValue函数加载到内存中;

2)Jvm在执行getValue函数的时候,由于数组下标index的值3超过了数组的最大下标的范围,所以在这里发生了异常问题,ArrayIndexOutOfBoundsException,这样导致程序就不会向下执行,jvm会在发生异常的地方停止,jvm会将发生异常信息(发生异常的位置、异常内容、异常类型等)封装到一个类中(Java的异常类),然后把这个封装了异常信息类的对象(new 异常类)丢给了当前调用函数的地方。

3)如果发生异常,jvm会自动创建封装了异常信息的异常类的对象,然后将对象使用throw关键字抛给调用getValue函数的地方。

4)由于getValue函数把问题抛给了main函数,所以导致了main函数中也有了异常,而main函数中的异常是被迫接收的,此时main函数中并没有解决此异常的解决方案,但是main函数是jvm调用的,所以main函数又将异常抛给了jvm虚拟机,jvm已经是底层了,不能再将异常抛出,jvm需要对这个问题进行处理,即将这个问题显示到屏幕上,让程序的使用者看到。

1.3 异常简单应用举例

需求:代码和上述代码相同,我们要解决上述问题的发生。

1)由于在自定义函数中两个参数都是接收外界传递过来的,我们为了保证程序的健壮(合法)性,

所以我们要对传递过来的数据进行合法性的判断;

2)分别对下标和数组进行判断,如果不合法,将发生问题的异常抛出去给程序员看;

/*
	针对发生的异常进行简单的处理
*/
class ExceptionDemo1
{
	public static void main(String[] args) 
	{
		//定义数组
		int[] arr={1,2,5};
		//int value=getValue(arr,1);
		int value=getValue(arr,1);
		System.out.println(value);
	}
	//定义函数根据指定的下标返回对应的值
	public static int getValue(int[] arr,int index)
	{
		/*
			以后在开发中,定义函数的时候,对外界传递过来的参数一定要
			合法性的判断
			这里需要对错误数据进行判断,然后将错误信息报告给调用者
			在实际开发中我们一般会给固定的文件写错误信息(错误文档)
		*/
		//System.out.println("haha");
		/*
		 * 发生空指针异常的地方,一定是使用了某个引用变量,而这个引用变量又不指向任何的对象
		 * 也就是说引用变量中保存的是null。这是使用空的引用调用属性或行为,而由于根本引用不指向
		 * 任何的对象,那么就没有属性和行为而言,更无法去调用了。肯定就发生空指针异常。
		 */
		if(arr==null)
		{
			throw new NullPointerException("对不起,数组引用变量的值不能为null");
		}
		if(index<0 || index>=arr.length)
		{
			throw new ArrayIndexOutOfBoundsException("下标越界了。。。");
		}
		return arr[index];
	}
}

1.4 异常体系

我们书写程序,肯定会有问题的发生,这些问题统称为异常。而sun公司把最常见的一些异常进行类的描述和封装。然后我们如果在程序中遇到了这些问题,就可以直接通过这些描述异常的类进行错误信息的封装。然后把这些信息丢给程序的调用者。

异常的根类是java.lang.Throwable,Throwable这个类描述的是Java中所有异常和错误的共性内容。其下有两个子类:java.lang.Errorjava.util.Exception,平常所说的异常指java.util.Exception

Throwable体系:

  • Error:严重错误Error,无法通过处理的错误,只能事先避免,好比绝症。

    ​ 在程序运行时,会产生一些错误信息。java把这些错误信息使用Error或其子类进行描述。

    错误属于系统级别的,是由于JVM在操作内存时(JVM需要借助操作系统来实现内存的操作),出现了一些不正常的操作,造成内存错误,出现错误后操作系统就会把这个错误返回给JVM。

    在程序中,遇到错误时,java没有针对性的解决方案,只能通过修改源代码的方式来解决程序中的错误问题。

  • Exception:表示异常,异常产生后程序员可以通过代码的方式纠正,使程序继续运行,是必须要处理的。好比感冒。

    ​ 在程序运行时,也会出现一些异常状况。表示Java程序中存在的异常问题,而不是错误问题。这些异常问题,在程序中通过判断等形式是可以检测并且预防的。针对这些异常问题,程序员在写代码的时候一旦发生,必须给出有效的解决方案。

    java对于异常状况是有针对性的解决方案(异常处理),例:角标越界、空指针异常等。

    异常状况的发生,通常是JVM在操作一些数据时,出现的问题,java对于异常的发生,是可以通过一些手段(捕获)避免程序终止运行,保证让程序继续向下正常执行。

1.5 异常分类

我们平常说的异常就是指Exception,因为这类异常一旦出现,我们就要对代码进行更正,修复程序。

异常(Exception)的分类:根据在编译时期还是运行时期去检查异常?

  • 编译时期异常:checked异常。在编译时期,就会检查,如果没有处理异常,则编译失败。(如日期格式化异常)
  • 运行时期异常:runtime异常。在运行时期,检查异常.在编译时期,运行异常不会让编译器检测(不报错)。

小结:

RuntimeException和Exception有什么区别:

Exception属于编译时异常,编译器在编译时会检测该异常是否异常的处理方案 ,如果没有处理方案,编译不能通过。

RuntimeException属于运行时异常,编译器不会检测该异常是否有异常的处理方案,不需要声明。

说明:在Exception的所有子类异常中,只有RuntimeException不是编译异常,是运行时异常,其他子类都是编译异常。

第二章 异常的处理

Java异常处理的五个关键字:try、catch、finally、throw、throws

2.1 抛出异常throw

在编写程序时,我们必须要考虑程序出现问题的情况。比如,在定义方法时,方法需要接受参数。那么,当调用方法使用接受到的参数时,首先需要先对参数数据进行合法的判断,数据若不合法,就应该告诉调用者,传递合法的数据进来。这时需要使用抛出异常的方式来告诉调用者。

在java中,提供了一个throw关键字,它用来抛出一个指定的异常对象。那么,抛出一个异常具体如何操作呢?

  1. 创建一个异常对象。封装一些提示信息(信息可以自己编写)。

  2. 需要将这个异常对象告知给调用者。怎么告知呢?怎么将这个异常对象传递到调用者处呢?通过关键字throw就可以完成。throw 异常对象。

    throw用在方法内,用来抛出一个异常对象,将这个异常对象传递到调用者处,并结束当前方法的执行。

使用格式:

throw new 异常类名(参数);

例如:

throw new NullPointerException("要访问的arr数组不存在");

throw new ArrayIndexOutOfBoundsException("该索引在数组中不存在,已超出范围");

学习完抛出异常的格式后,我们通过下面程序演示下throw的使用。

public class ThrowDemo {
    public static void main(String[] args) {
        //创建一个数组 
        int[] arr = {2,4,52,2};
        //根据索引找对应的元素 
        int index = 4;
        int element = getElement(arr, index);

        System.out.println(element);
        System.out.println("over");
    }
    /*
     * 根据 索引找到数组中对应的元素
     */
    public static int getElement(int[] arr,int index){ 
       	//判断  索引是否越界
        if(index<0 || index>arr.length-1){
             /*
             判断条件如果满足,当执行完throw抛出异常对象后,方法已经无法继续运算。
             这时就会结束当前方法的执行,并将异常告知给调用者。这时就需要通过异常来解决。 
              */
             throw new ArrayIndexOutOfBoundsException("哥们,角标越界了~~~");
        }
        int element = arr[index];
        return element;
    }
}

注意:如果产生了问题,我们就会throw将问题描述类即异常进行抛出,也就是将问题返回给该方法的调用者。

那么对于调用者来说,该怎么处理呢?一种是进行捕获处理,另一种就是继续将问题声明出去,使用throws声明处理。

2.2 声明异常throws(掌握)

声明异常:将问题标识出来,报告给调用者。如果方法内通过throw抛出了编译时异常,而没有捕获处理(稍后讲解该方式),那么必须通过throws进行声明,让调用者去处理。

关键字throws运用于方法声明之上,用于表示当前方法不处理异常,而是提醒该方法的调用者来处理异常(抛出异常).

声明异常格式:

修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2…
{   
	方法体
}	

注意:

​ 1)throws后面可以跟多个异常类,使用逗号分隔;

2)在函数中如果发生了异常,jvm会拿发生的异常和声明的异常类进行匹配,如果匹配才会把发生的异常交给别人处理;

需求:声明的简单使用

1)定义一个ThrowDemo类;

2)在定义一个Demo类,在这个类中定义一个函数show,在show函数中对传入的参数x进行判断,如果x等于0,则使用throw关键字抛出Exception异常;

3)由于抛出的是编译时异常所以需要在show函数上使用throws关键字声明这个异常,告诉调用者,有异常;

4)在show函数中打印x的值;

5)在ThrowDemo类中创建Demo类的对象,使用对象调用函数;

声明异常的代码演示:

public class ThrowsDemo {
    public static void main(String[] args) throws Exception {
        show(2);
    }

    // 如果定义功能时有问题发生需要报告给调用者。可以通过在方法上使用throws关键字进行声明
    public static void show(int x) throws Exception {
        if (x == 0) {//如果x等于0
            // 我假设  如果不是 a.txt 认为 该文件不存在 是一个错误 也就是异常  throw
            throw new Exception("x不能等于0");
        }
       System.out.println(x);
    }
}

注意:在开发中,main函数中不会出现声明,在main函数通常是使用捕获。

2.3 捕获异常try…catch(掌握)

2.3.1格式

就是遇到异常时,不再把异常交给他人处理,自己处理。

在程序中有异常,但这个异常我们不能继续使用throws声明,这时不处理,程序无法编译通过,那么在程序中只能使用捕获方式解决问题。

格式如下:

try{
     编写可能会出现异常的代码
}catch(异常类型  对象名){
     处理异常的代码
     //记录日志/打印异常信息/继续抛出异常
}

try:该代码块中编写可能产生异常的代码。

catch:用来进行某种异常的捕获,实现对捕获到的异常进行处理。

注意:try和catch都不能单独使用,必须连用。

演示如下:

public class ThrowDemo {
	 /*
     * 一般在开发中我们不会在主函数上面抛异常,主函数一般是最后处理者,
     * 我们需要在主函数中对异常进行处理------》捕获
     */
    public static void main(String[] args){

        try
        {
            show(0);//异常代码
        }catch(Exception e)//Exception e=new Exception("x不能等于0")
        {
            //处理异常的代码
            //System.out.println("hahhahaha");
			//System.out.println(e.getMessage());//x不能等于0
//			System.out.println(e);
	        e.printStackTrace();
        }
    }
    public static void show(int x) throws Exception {
        if (x == 0) {//如果x等于0
           
            throw new Exception("x不能等于0");
        }
        System.out.println(x);
    }
}

总结:不管自己是函数的定义者,还是函数的调用者,只要是在自己的函数中有异常发生, 那么自己都可以使用上述的两种方案对异常进行处理。

2.3.2如何获取异常信息:

Throwable类中定义了一些查看方法:

异常中的常用方法

方法 说明
getMessage() 获取报错原因.获取异常的描述信息,原因(提示给用户的时候,就提示错误原因)。
toString() 获取报错的类型和原因
printStackTrace() 直接打印报错的类型、原因和位置.包含了异常的类型,异常的原因,还包括异常出现的位置.

代码演示:

public class Test04 {
    public static void main(String[] args) {

        //解析异常(编译时期异常)
        String s = "2000-11-12";
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");

        try {
            //解析
            Date date = sdf.parse(s);
            System.out.println(date);

        }catch (ParseException e){
            //e是一个异常对象
            //1.getMessage()获取报错原因
//            String ss = e.getMessage();
//            System.out.println(ss);      //Unparseable date: "2000-11-12"


            //2.toString()获取报错的类型和原因
//            String ss = e.toString();
//            System.out.println(ss);       //java.text.ParseException: Unparseable date: "2000-11-12"


            //3.printStackTrace()直接打印报错的类型、原因和位置
            /*
               java.text.ParseException: Unparseable date: "2000-11-12"
                at java.base/java.text.DateFormat.parse(DateFormat.java:388)
                at com.itheima_03.Demo05_tryCatch.main(Demo05_tryCatch.java:17)
            */
            e.printStackTrace();
        }

        //当trycatch执行完程序会继续往后执行
    }
}

2.3.3 捕获异常代码实践

定义一个方法接收一个生日日期字符串,计算生日日期到当前日期的天数并返回。

main方法中让用户输入一个生日日期字符串,调用设计好的方法计算在地球上活了多少天。

要求:如果解析发生异常,捕获异常,提示用户要重新输入生日日期字符串,直到输入正确的日期为止。

思考:设计代码的过程中想想什么时候捕获异常,什么时候声明异常?

package com.itheima.sh.k_date_11;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Scanner;

/*
    计算出生天数:
    步骤:
    1.创建键盘录入的对象
    2.获取录入的生日
    3.创建日期格式化解析类对象
    4.使用日期格式化解析类对象调用解析方法将录入的生日的字符串解析为Date
    5.将解析后的Date对象转换为毫秒
    6.获取当前系统时间
    7.将当前系统的Date时间转换为毫秒
    8.将当前系统时间毫秒和生日的毫秒进行差值运算然后转换为天数,并输出
 */
public class DateFormatDemo05 {
    public static void main(String[] args) {
        //循环控制
        while (true) {
            try {
                //1.创建键盘录入的对象
                Scanner sc = new Scanner(System.in);
                //2.获取录入的生日
                System.out.println("请输入您的生日(yyyy-MM-dd):");
                String inputBirthdayStr = sc.nextLine();
                //调用方法获取出生天数
                long day = getDays(inputBirthdayStr);
                //8.将当前系统时间毫秒和生日的毫秒进行差值运算然后转换为天数,并输出
                System.out.println("您出生了:" + day + "天");
                //停止死循环
                break;
            } catch (ParseException e) {
                System.out.println("日期格式不对,请重新输入日期(yyyy-MM-dd)");
            }
        }

    }

    public static long getDays(String inputBirthdayStr) throws ParseException {
        //3.创建日期格式化解析类对象
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        //4.使用日期格式化解析类对象调用解析方法将录入的生日的字符串解析为Date
        Date birthdayDate = sdf.parse(inputBirthdayStr);
        //5.将解析后的Date对象转换为毫秒
        long birthdayTime = birthdayDate.getTime();
        //6.获取当前系统时间
        Date nowDate = new Date();
        //7.将当前系统的Date时间转换为毫秒
        long nowTime = nowDate.getTime();
        //计算天数
        long day = (nowTime - birthdayTime) / 1000 / 60 / 60 / 24;
        //返回天数
        return day;
    }
}

2.3.4捕获多个异常

多个异常使用捕获又该如何处理呢?

​ 多个异常一次捕获,多次处理。

一般我们是使用一次捕获多次处理方式,格式如下:

try{
     编写可能会出现异常的代码
}catch(异常类型A  e){  当try中出现A类型异常,就用该catch来捕获.
     处理异常的代码
     //记录日志/打印异常信息/继续抛出异常
}catch(异常类型B  e){  当try中出现B类型异常,就用该catch来捕获.
     处理异常的代码
     //记录日志/打印异常信息/继续抛出异常
}

2.4 finally 代码块(掌握)

finally:有一些特定的代码无论异常是否发生,都需要执行。另外,因为异常会引发程序跳转,导致有些语句执行不到。而finally就是解决这个问题的,在finally代码块中存放的代码都是一定会被执行的。

什么时候的代码必须最终执行?

当我们在try语句块中打开了一些物理资源(磁盘文件/网络连接/数据库连接等),我们都得在使用完之后,最终关闭打开的资源。

finally的语法:

try...catch....finally:自身需要处理异常,最终还得关闭资源。

try{
	   可能发生异常的代码
   }catch(异常类名  变量名){
	  处理异常的代码。
	}finally{
	 程序中永远都能执行到的代码
  }

在程序中随时都有可能发生异常,一旦程序中发生了异常,这样就会导致在发生异常的地方之后的所有代码都不会运行。可是在程序中,有时有些代码不管程序中有没有问题都必须执行。这时这部分代码必须写在finally代码块中。

注意:finally不能单独使用。

比如在我们之后学习的IO流中,当打开了一个关联文件的资源,最后程序不管结果如何,都需要把这个资源关闭掉。

finally代码参考如下:

需求:

1)创建一个Test类;

2)定义一个Demo1类,在这个类中定义一个show函数,根据传递进来的参数x进行判断,如果x等于0,则使用throw关键字抛异常Exception;

3)对这个异常我们不使用声明处理,我们使用捕获进行处理,即使用try-catch-finally进行处理;

4)并分别在try-catch-finally的代码中使用return关键字返回给调用者1,2,3;

5)在Test类中创建Demo1对象,并使用对象调用类中的show函数,并打印返回来的值;

package cn.itcast.sh.b_excep_other;
class Demo1
{
	public int show(int x)
	{
		try
		{
			if(x==0){
				throw new Exception("x是零");
			}
			System.out.println("try.....");
			return 1;
		}catch(Exception e)
		{
			System.out.println("捕获异常");
			return 2;
		}finally
		{
			System.out.println("必须执行");
			return 3;
		}
	}
}
public class Test {
	public static void main(String[] args) {
		Demo1 d = new Demo1();
		int value=d.show(0);
		System.out.println(value);
	}
}

说明:在上述代码中,因为传入show函数中的值是0,所以发生异常,那么在jvm执行catch中的return 2代码的时候,jvm发现此时下面有finally关键字,那么jvm先不会执行return 2语句,jvm会先去执行finally中的代码,因为此时finally中有return 3语句,在函数中,遇见return关键字就会结束整个函数,此时jvm不会回来执行return2语句,如果在finally代码块中没有return语句,那么jvm在执行finally代码块里面的代码之后就又会回到catch中继续执行return 2语句。

finally代码块是永远都会被执行的代码,不管程序发生什么问题,最后JVM一定会把finally中的代码执行一遍。

使用场景

之后会学习IO流,IO流就相当于水龙头。

打开水龙头之后,中间不管是做了什么操作,最后都一定要关闭水龙头。(节约资源)

关闭水龙头(节约资源)的代码就可以写在finally

2.5 异常注意事项

  • 运行时异常被抛出可以不处理。即不捕获也不声明抛出。

  • 父类方法没有抛出异常,子类覆盖父类该方法时也不可抛出异常。此时子类产生该异常,只能捕获处理,不能声明抛出

    需求:

    ​ 1)创建类Test2;

    ​ 2)定义一个Fu类和Zi类,在Fu类中定义一个method函数;

    ​ 3)在Zi类中复写method函数,在这个函数中抛Exception异常;

    ​ 4)在类Test2中创建Zi类对象,通过子类对象调用子类中的method函数;

    ​ 5)在Zi类中的method中捕获异常;

package cn.itcast.sh.b_excep_other;
class Fu
{
	public void method()
	{
		System.out.println("父类中的method");
	}
}
class Zi extends Fu
{
	public void method() //throws Exception
	{
		System.out.println("子类重写方法method");
		try {
			throw new Exception("异常");
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}
public class Test2 {
	public static void main(String[] args) {
		Zi z=new Zi();
		z.method();
	}
}
  • 在try/catch后可以追加finally代码块,其中的代码一定会被执行,通常用于资源回收。

  • 如果finally有return语句,永远返回finally中的结果,避免该情况.

    需求:

    1)创建一个Test类;

    2)定义一个Demo1类,在这个类中定义一个show函数,根据传递进来的参数x进行判断,如果x等于0,则使用throw关键字抛异常Exception;

    3)对这个异常我们不使用声明处理,我们使用捕获进行处理,即使用try-catch-finally进行处理;

    4)并分别在try-catch-finally的代码中使用return关键字返回给调用者1,2,3;

    5)在Test类中创建Demo1对象,并使用对象调用类中的show函数,并打印返回来的值;

    

    package cn.itcast.sh.b_excep_other;
    class Demo1
    {
    	public int show(int x)
    	{
    		try
    		{
    			if(x==0){
    				throw new Exception("x是零");
    			}
    			System.out.println("try.....");
    			return 1;
    		}catch(Exception e)
    		{
    			System.out.println("捕获异常");
    			return 2;
    		}finally
    		{
    			System.out.println("必须执行");
    			return 3;
    		}
    	}
    }
    public class Test {
    	public static void main(String[] args) {
    		Demo1 d = new Demo1();
    		int value=d.show(0);
    		System.out.println(value);
    	}
    }
    

第三章 自定义异常

3.1 概述

为什么需要自定义异常类:

我们说了Java中不同的异常类,分别表示着某一种具体的异常情况,那么在开发中总是有些异常情况是SUN没有定义好的,此时我们根据自己业务的异常情况来定义异常类。,例如年龄负数问题,考试成绩负数问题。

在上述代码中,发现这些异常都是JDK内部定义好的,但是实际开发中也会出现很多异常,这些异常很可能在JDK中没有定义过,例如年龄负数问题,考试成绩负数问题.那么能不能自己定义异常呢?

什么是自定义异常类:

在开发中根据自己业务的异常情况来定义异常类.

自定义一个业务逻辑异常: LoginException。一个登陆异常类。

异常类如何定义:

  1. 自定义一个编译期异常: 自定义类 并继承于java.lang.Exception

  2. 自定义一个运行时期的异常类:自定义类 并继承于java.lang.RuntimeException

    异常也是一个类,那么就和我们学习面向对象中定义类没有区别:

    格式:

    public class 异常类的名字  extends Exception / RuntimeException
    
    {
    
    	//不需要任何的属性和行为,仅仅只需要提供构造函数即可。
    	public 异常类的名字(){}
    	public 异常类的名字( String message ){
    	super(message);
    	}
    }
    

注意:不需要任何的属性和行为,仅仅只需要提供构造函数即可。

查看异常源码

public class NullPointerException extends RuntimeException {
    //版本号 没什么用
    private static final long serialVersionUID = 5162710183389028792L;
	//构造方法
    public NullPointerException() {
        super();
    }
	//构造方法
    public NullPointerException(String s) {
        super(s);
    }
}
    • 通过查看源码我们发现,异常类中什么都没有定义。
    • 不同的异常只是类名不同而已,内部并没有特殊的方法。类名不同方便程序员识别不同的错误。
  • 自定义异常的作用

    • 自定义异常的作用就是在出现异常时,让异常的名字更加直观。

3.2 自定义异常的练习

练习:

需求:定义一个类,描述矩形,提供计算面积的功能。要求对长和宽进行判断, 如果非法直接抛出长或宽非法异常。

分析和步骤:

1)定义一个类Rectangle描述矩形,在这个类中分别定义长length和宽width两个私有属性,并对外提供get和set方法;

2)在Rectangle类中定义一个构造函数分别给长length和宽width初始化值;

3)在这个类的构造函数中分别对length和width两个属性进行判断,如果长和宽不合法,分别对长和宽进行抛异常,同时构造函数要声明异常;

4)定义一个类IllegalWidthException对非法的宽进行异常处理,在这个类中分别定义无参构造函数和有一个参数的构造函数,同时这个类需要继承Exception类,这样在编译的时候就可以检测异常;

5)定义一个类IllegalLengthException对非法的长进行异常处理,在这个类中分别定义无参构造函数和有一个参数的构造函数,同时这个类需要继承Exception类,这样在编译的时候就可以检测异常;

6)在Rectangle类中定义一个计算矩形面积的函数getArea,将最终的面积值返回给调用者;

7)定义一个异常测试类ThrowTest,在这个测试类中创建矩形类Rectangle的对象,并通过对象调用计算圆的面积的函数getArea并打印,同时并对异常进行捕获处理;

/*
 * 需求:定义一个类,描述矩形,提供计算面积的功能。
 * 要求对长和宽进行判断, 如果非法直接抛出长或宽非法异常。
 */
//自己定义长和宽非法的异常
class IllegalWidthException extends Exception{

	public IllegalWidthException() {
		super();
	}

	public IllegalWidthException(String message) {
		super(message);
	}
}

//非法的长度异常
class IllegalLengthException extends Exception{

	public IllegalLengthException() {
		super();
	}
	public IllegalLengthException(String message) {
		super(message);
	}
}

//描述矩形
class Rectangle{
	//长和宽
	private double length;
	private double width;
	
	public Rectangle(double length, double width) throws IllegalLengthException, IllegalWidthException {
		//对长和宽进行合法的验证
		if( length <= 0 ){
			throw new IllegalLengthException("非法的矩形长度");
		}
		
		if( width <= 0 ){
			throw new IllegalWidthException("非法的矩形宽度");
		}
		this.length = length;
		this.width = width;
	}
	
	public double getLength() {
		return length;
	}
	public void setLength(double length) {
		this.length = length;
	}
	public double getWidth() {
		return width;
	}
	public void setWidth(double width) {
		this.width = width;
	}
	
	//计算面积
	public double getArea(){
		return this.length * this.width;
	}
	
}

public class ThrowTest {
	public static void main(String[] args) {
		
		//创建矩形对象
		try {
			Rectangle r = new Rectangle(-3,4);
			
			System.out.println(r.getArea());
			
		} catch ( IllegalWidthException e) {
			e.printStackTrace();
		} catch ( IllegalLengthException e) {
			e.printStackTrace();
		}
		
	}
}

3.3 关于捕获多个异常注意事项

一般我们是使用一次捕获多次处理方式,格式如下:

try{
     编写可能会出现异常的代码
}catch(异常类型A  e){  当try中出现A类型异常,就用该catch来捕获.
     处理异常的代码
     //记录日志/打印异常信息/继续抛出异常
}catch(异常类型B  e){  当try中出现B类型异常,就用该catch来捕获.
     处理异常的代码
     //记录日志/打印异常信息/继续抛出异常
}

注意:这种异常处理方式,要求多个catch中的异常不能相同,并且若catch中的多个异常之间有子父类异常的关系,那么子类异常要求在上面的catch处理,父类异常在下面的catch处理。

关于捕获多个异常注意事项代码演示如下:

需求:

1)定义一个Demo类,在这个类中分别定义主函数和一个show函数;

2)在分别定义两个异常类A和B;

3)A类继承Exception类,并创建两个构造函数;

4)B类继承A类,并创建两个构造函数;

5)在Demo类中的主函数中调用show函数,并传参0;

6)在Demo类中的show函数中分别写三个判断语句,使用throw关键字分别抛出B、A和Exception类三个异常类的对象,同时在show函数声明三个异常;

7)这样在main函数中就得对异常语句(调用show函数语句)进行捕获异常处理;

package cn.itcast.sh.b_excep_other;
//自定义异常类
class A extends Exception
{
	public A() {
		super();
	}
	public A(String message) {
		super(message);
	}
}
class B extends A
{
	public B() {
		super();
	}
	public B(String message) {
		super(message);
	}
}
public class Demo {
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		try
		{
			show(0);//new Exception() new A() new B()
			//这里可能发生A B Exception异常中的某一个异常
			/*
			 * Exception e =new Exception() 
			 * Exception e=new A()
			 * Exception e=new B()
			 */
		}catch(B b)
		{
			
		}catch(A a)
		{
			
		}catch(Exception e)//
		{
			
		}
	}
	public static void show(int x) throws B,A,Exception
	{
		if(x==0)
		{
            throw new B();
		}
		if(x==1)
		{
			throw new A();
		}
		if(x==2)
		{
			throw new Exception();
		}
		
	}
}

第四章 Lambda表达式

1 函数式编程思想概述

在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿什么东西做什么事情”。相对而言,面向对象过分强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以什么形式做

做什么,而不是怎么做

例如之前的匿名内部类方式实现多线程案例中。

我们真的希望创建一个匿名内部类对象吗?不。我们只是为了做这件事情而不得不创建一个对象。我们真正希望做的事情是:将run方法体内的代码传递给Thread类知晓。

传递一段代码——这才是我们真正的目的。而创建对象只是受限于面向对象语法而不得不采取的一种手段方式。那,有没有更加简单的办法?如果我们将关注点从“怎么做”回归到“做什么”的本质上,就会发现只要能够更好地达到目的,过程与形式其实并不重要。

2 Lambda的优化

当需要启动一个线程去完成任务时,通常会通过java.lang.Runnable接口来定义任务内容,并使用java.lang.Thread类来启动该线程。

传统写法,代码如下:

public class Demo03Thread {
	public static void main(String[] args) {
		new Thread(new Runnable() {
			@Override
			public void run() {
				System.out.println("多线程任务执行!");
			}
		}).start();
	}
}

本着“一切皆对象”的思想,这种做法是无可厚非的:首先创建一个Runnable接口的匿名内部类对象来指定任务内容,再将其交给一个线程来启动。

代码分析:

对于Runnable的匿名内部类用法,可以分析出几点内容:

  • Thread类需要Runnable接口作为参数,其中的抽象run方法是用来指定线程任务内容的核心;
  • 为了指定run的方法体,不得不需要Runnable接口的实现类;
  • 为了省去定义一个RunnableImpl实现类的麻烦,不得不使用匿名内部类;
  • 必须覆盖重写抽象run方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;
  • 而实际上,似乎只有方法体才是关键所在

Lambda表达式写法,代码如下:

借助Java 8的全新语法,上述Runnable接口的匿名内部类写法可以通过更简单的Lambda表达式达到等效:

public class Demo04LambdaRunnable {
	public static void main(String[] args) {
		new Thread(() -> System.out.println("多线程任务执行!")).start(); // 启动线程
	}
}

这段代码和刚才的执行效果是完全一样的,可以在1.8或更高的编译级别下通过。从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。

不再有“不得不创建接口对象”的束缚,不再有“抽象方法覆盖重写”的负担,就是这么简单!

3 Lambda的格式

标准格式:

Lambda省去面向对象的条条框框,格式由3个部分组成:

  • 一些参数
  • 一个箭头
  • 一段代码

Lambda表达式的标准格式为:

(参数类型 参数名称,参数类型 参数名称,..) -> { 代码语句 }

格式说明:

  • 小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。
  • ->是新引入的语法格式,代表指向动作。
  • 大括号内的语法与传统方法体要求基本一致。

匿名内部类与lambda对比:

new Thread(new Runnable() {
			@Override
			public void run() {
				System.out.println("多线程任务执行!");
			}
}).start();

仔细分析该代码中,Runnable接口只有一个run方法的定义:

  • public abstract void run();

即制定了一种做事情的方案(其实就是一个方法):

  • 无参数:不需要任何条件即可执行该方案。
  • 无返回值:该方案不产生任何结果。
  • 代码块(方法体):该方案的具体执行步骤。

同样的语义体现在Lambda语法中,要更加简单:

() -> System.out.println("多线程任务执行!")
  • 前面的一对小括号即run方法的参数(无),代表不需要任何条件;
  • 中间的一个箭头代表将前面的参数传递给后面的代码;
  • 后面的输出语句即业务逻辑代码。

参数和返回值:

下面举例演示java.util.Comparator<T>接口的使用场景代码,其中的抽象方法定义为:

  • public abstract int compare(T o1, T o2);

当需要对一个对象数组进行排序时,Arrays.sort方法需要一个Comparator接口实例来指定排序的规则。假设有一个Person类,含有String nameint age两个成员变量:

public class Person { 
    private String name;
    private int age;
    
    // 省略构造器、toString方法与Getter Setter 
}

传统写法

如果使用传统的代码对Person[]数组进行排序,写法如下:

public class Demo05Comparator {
    public static void main(String[] args) {
      	// 本来年龄乱序的对象数组
        Person[] array = { new Person("古力娜扎", 19),new Person("迪丽热巴", 18), new Person("马尔扎哈", 20) };

      	// 匿名内部类
        Comparator<Person> comp = new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.getAge() - o2.getAge();
            }
        };
        Arrays.sort(array, comp); // 第二个参数为排序规则,即Comparator接口实例

        for (Person person : array) {
            System.out.println(person);
        }
    }
}

这种做法在面向对象的思想中,似乎也是“理所当然”的。其中Comparator接口的实例(使用了匿名内部类)代表了“按照年龄从小到大”的排序规则。

代码分析

下面我们来搞清楚上述代码真正要做什么事情。

  • 为了排序,Arrays.sort方法需要排序规则,即Comparator接口的实例,抽象方法compare是关键;
  • 为了指定compare的方法体,不得不需要Comparator接口的实现类;
  • 为了省去定义一个ComparatorImpl实现类的麻烦,不得不使用匿名内部类;
  • 必须覆盖重写抽象compare方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;
  • 实际上,只有参数和方法体才是关键

Lambda写法

public class Demo06ComparatorLambda {
    public static void main(String[] args) {
        Person[] array = {
          	new Person("古力娜扎", 19),
          	new Person("迪丽热巴", 18),
          	new Person("马尔扎哈", 20) };

        Arrays.sort(array, (Person a, Person b) -> {
          	return a.getAge() - b.getAge();
        });

        for (Person person : array) {
            System.out.println(person);
        }
    }
}

省略格式:

省略规则

在Lambda标准格式的基础上,使用省略写法的规则为:

  1. 小括号内参数的类型可以省略;
  2. 如果小括号内有且仅有一个参数,则小括号可以省略;
  3. 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号。

备注:如果省略大括号、return关键字及语句分号的原则要省略都省略,要么都不能省略。

可推导即可省略

Lambda强调的是“做什么”而不是“怎么做”,所以凡是可以推导得知的信息,都可以省略。例如上例还可以使用Lambda的省略写法:

Runnable接口简化:
1. () -> System.out.println("多线程任务执行!")
Comparator接口简化:
2. Arrays.sort(array, (a, b) -> a.getAge() - b.getAge());

4 Lambda的前提条件

Lambda的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:

  1. 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法
    无论是JDK内置的RunnableComparator接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。
  2. 使用Lambda必须具有接口作为方法参数。
    也就是方法的参数必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。

备注:有且仅有一个抽象方法的接口,称为“函数式接口”。

5使用lambda总结

匿名内部类:
	可以用于类也可以用于接口,对类和接口中的方法的个数没有要求。
	
Lambda表达式:
	只能用于接口,接口中抽象方法只能有一个。
	
Lambda表达式的要求更严格,并不是所有的匿名内部类都能改成Lambda表达式。

第五章 函数式接口

1 概述

函数式接口在Java中是指:有且仅有一个抽象方法的接口

函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。

格式

只要确保接口中有且仅有一个抽象方法即可:

修饰符 interface 接口名称 {
    public abstract 返回值类型 方法名称(可选参数信息);
    // 其他非抽象方法内容
}

由于接口当中抽象方法的public abstract是可以省略的,所以定义一个函数式接口很简单:

public interface MyFunctionalInterface {	
	void myMethod();
}

自定义函数式接口

对于刚刚定义好的MyFunctionalInterface函数式接口,典型使用场景就是作为方法的参数:

public class Demo07FunctionalInterface {	
	// 使用自定义的函数式接口作为方法参数
	private static void doSomething(MyFunctionalInterface inter) {
		inter.myMethod(); // 调用自定义的函数式接口方法
	}
	
	public static void main(String[] args) {
		// 调用使用函数式接口的方法
		doSomething(() -> System.out.println("Lambda执行啦!"));
	}
}

FunctionalInterface注解

@Override注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解:@FunctionalInterface。该注解可用于一个接口的定义上:

@FunctionalInterface
public interface MyFunctionalInterface {
	void myMethod();
}

一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。不过,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样。

2 常用函数式接口

JDK提供了大量常用的函数式接口以丰富Lambda的典型使用场景,它们主要在java.util.function包中被提供。下面是两个常用的函数式接口及使用示例。

Supplier接口

java.util.function.Supplier<T>接口仅包含一个无参的方法:T get()。用来获取一个泛型参数指定类型的对象数据。由于这是一个函数式接口,这也就意味着对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象数据。

说明:

​ 1、这里的泛型T表示当我们使用该接口时,传递什么数据类型就是什么数据类型。然后我们通过这个接口中的get方法就可以获取到指定泛型的类型的对象。

​ 举例:比如使用该接口时,指定泛型T是String类型,那么通过get()方法就可以获取String类的对象。

​ 举例:比如使用该接口时,指定泛型T是Student类型,那么通过get()方法就可以获取Student类的对象。

​ 2、由于Supplier属于函数式接口,我们可以使用Lambda来实现。在Lambda中可以完成Supplier接口中的get()方法体中的代码。主要完成生产某个类的对象数据的功能。即我们需要使用Lambda返回指定泛型类的对象。

3、其实Supplier接口就是用来生产某个类的对象的。

需求:指定泛型是String类型,获取String类的对象。

代码实现如下所示:

/*
    需求:指定泛型是String类型,获取String类的对象。
 */
public class SupplierDemo {
    public static void main(String[] args) {
        //调用方法
        String s = getString(() -> {
            return "hello";
        });
        //输出获取到的String类的对象s
        System.out.println("s = " + s);
    }
    //定义方法获取String类的对象
    public static String getString(Supplier<String> lambda)//Supplier<String> 已经确定泛型T是String类型
    {
        return lambda.get();
    }
}

说明:上述只是生产String类的对象,那么我们要生产任何类的对象,我们可以修改代码如下所示:

public class SupplierDemo01 {
    public static void main(String[] args) {
        //调用方法
        String s = getObject(() -> {
            return "hello";
        });
        //输出获取到的String类的对象s
        System.out.println("s = " + s);
        //获取Integer类的对象
        Integer i = getObject(() -> {
            return 123;
        });
        //输出
        System.out.println("i = " + i);
    }
    //定义方法获取任意类的对象
    public static <T> T getObject(Supplier<T> lambda)//这样定义会更加方便,可以获取任意类型的对象
    {
        return lambda.get();
    }
}

问题:

有的同学会有疑问:如下述代码:

 Integer i = getObject(() -> {
            return 123;
        });

我还不如直接定义:Integer i =123;或者int i=123;这样定义更加简单。为什么还要使用Supplier接口方式,这么麻烦。

原因:

​ 使用Integer i =123;或者int i=123;这样定义,就把获取对象数据的方式写死了、固定了。很不灵活。

而使用Supplier接口的方式,我们可以在getObject方法中,除了执行return lambda.get();我们还可以书写其他的代码,完成其他的功能。比如再生产某个对象时,有可能我们还会结合其他的类和对象完成更多的功能。只是我们这里书写的比较简单,简化了业务代码。

Consumer接口

java.util.function.Consumer<T>接口,是消费一个数据,其数据类型由泛型参数决定。

抽象方法:accept

Consumer接口中包含抽象方法void accept(T t): 消费一个指定泛型的数据。

基本使用如:

//给你一个字符串,请按照大写的方式进行消费
import java.util.function.Consumer;
public class Demo08Consumer {
    public static void main(String[] args) {
        String str = "Hello World";
        //1.lambda表达式标准格式
        fun(str,(String s)->{
            System.out.println(s.toUpperCase());
        });
        //2.lambda表达式简化格式
        fun(str,s-> System.out.println(s.toUpperCase()));
    }
    /*
        定义方法,使用Consumer接口作为参数
        fun方法: 消费一个String类型的变量
     */
    public static void fun(String s,Consumer<String> con) {
        con.accept(s);
    }
}

Predicate接口

有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用java.util.function.Predicate<T>接口。

抽象方法:test

Predicate接口中包含一个抽象方法:boolean test(T t)。用于条件判断的场景:

//1.练习:判断字符串长度是否大于5
//2.练习:判断字符串是否包含"H"
public class Demo09Predicate {
    private static void method(Predicate<String> predicate,String str) {
        boolean veryLong = predicate.test(str);
        System.out.println("字符串很长吗:" + veryLong);
    }

    public static void main(String[] args) {
        method(s -> s.length() > 5, "HelloWorld");
    }
}

条件判断的标准是传入的Lambda表达式逻辑,只要字符串长度大于5则认为很长。

Function接口

java.util.function.Function<T,R>接口用来根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件,有进有出。

抽象方法:apply

Function接口中最主要的抽象方法为:R apply(T t),根据类型T的参数获取类型R的结果。

说明:

Function接口其实是一个转换接口,具有转换功能,可以将传入的T类型数据转换为R类型数据。转换的操作由Lambda表达式来处理,就是根据类型T作为参数的数据获取R类型的数据。

使用的场景例如:将String类型转换为Integer类型。

"123"------>转换为123。

/*
    使用的场景例如:将String类型转换为Integer类型。
    "123"------>转换为123。
 */
public class FunctionDemo {
    public static void main(String[] args) {
        //调用method方法
        /*int x = method("123", (s) -> {
            return Integer.parseInt(s);
        });*/
        int x = method("123", s->Integer.parseInt(s));
        //输出转换后的值
        System.out.println("x = " + x);
    }
    //定义方法通过接口Function将字符串转换为整数
    public static int method(String s,Function<String,Integer> lambda)
    {
        return lambda.apply(s);
    }
}

第六章 Stream(必须掌握)

在Java 8中,得益于Lambda所带来的函数式编程,引入了一个全新的Stream概念,用于解决已有集合类库既有的弊端。

1 引言

传统集合的多步遍历代码

几乎所有的集合(如Collection接口或Map接口等)都支持直接或间接的遍历操作。而当我们需要对集合中的元素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。例如:

public class Demo10ForEach {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("张无忌");
        list.add("周芷若");
        list.add("赵敏");
        list.add("张强");
        list.add("张三丰");
        for (String name : list) {
          	System.out.println(name);
        }
    }  
}

这是一段非常简单的集合遍历操作:对集合中的每一个字符串都进行打印输出操作。

循环遍历的弊端

Java 8的Lambda让我们可以更加专注于做什么(What),而不是怎么做(How),这点此前已经结合内部类进行了对比说明。现在,我们仔细体会一下上例代码,可以发现:

  • for循环的语法就是“怎么做
  • for循环的循环体才是“做什么

为什么使用循环?因为要进行遍历。但循环是遍历的唯一方式吗?遍历是指每一个元素逐一进行处理,而并不是从第一个到最后一个顺次处理的循环。前者是目的,后者是方式。

试想一下,如果希望对集合中的元素进行筛选过滤:

  1. 将集合A根据条件一过滤为子集B
  2. 然后再根据条件二过滤为子集C

那怎么办?在Java 8之前的做法可能为:

这段代码中含有三个循环,每一个作用不同:

  1. 首先筛选所有姓张的人;
  2. 然后筛选名字有三个字的人;
  3. 最后进行对结果进行打印输出。
public class Demo11NormalFilter {
  	public static void main(String[] args) {
      	List<String> list = new ArrayList<>();
        list.add("张无忌");
        list.add("周芷若");
        list.add("赵敏");
        list.add("张强");
        list.add("张三丰");

        List<String> zhangList = new ArrayList<>();
        for (String name : list) {
            if (name.startsWith("张")) {
              	zhangList.add(name);
            }
        }

        List<String> shortList = new ArrayList<>();
        for (String name : zhangList) {
            if (name.length() == 3) {
              	shortList.add(name);
            }
        }

        for (String name : shortList) {
          	System.out.println(name);
        }
    }
}

每当我们需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。这是理所当然的么?不是。循环是做事情的方式,而不是目的。另一方面,使用线性循环就意味着只能遍历一次。如果希望再次遍历,只能再使用另一个循环从头开始。

那,Lambda的衍生物Stream能给我们带来怎样更加优雅的写法呢?

Stream的更优写法

下面来看一下借助Java 8的Stream API,什么才叫优雅:

public class Demo12StreamFilter {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("张无忌");
        list.add("周芷若");
        list.add("赵敏");
        list.add("张强");
        list.add("张三丰");

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

直接阅读代码的字面意思即可完美展示无关逻辑方式的语义:获取流、过滤姓张、过滤长度为3、逐一打印。代码中并没有体现使用线性循环或是其他任何算法进行遍历,我们真正要做的事情内容被更好地体现在代码中。

2 流式思想概述

注意:请暂时忘记对传统IO流的固有印象!

整体来看,流式思想类似于工厂车间的“生产流水线”。

当需要对多个元素进行操作(特别是多步操作)的时候,考虑到性能及便利性,我们应该首先拼好一个“模型”步骤方案,然后再按照方案去执行它。

这张图中展示了过滤、映射、跳过、计数等多步操作,这是一种集合元素的处理方案,而方案就是一种“函数模型”。图中的每一个方框都是一个“流”,调用指定的方法,可以从一个流模型转换为另一个流模型。而最右侧的数字3是最终结果。

这里的filtermapskip都是在对函数模型进行操作,集合元素并没有真正被处理。只有当终结方法count执行的时候,整个模型才会按照指定策略执行操作。而这得益于Lambda的延迟执行特性。

备注:“Stream流”其实是一个集合元素的函数模型,它并不是集合,也不是数据结构,其本身并不存储任何元素(或其地址值)。

3 获取流方式

java.util.stream.Stream<T>是Java 8新加入的最常用的流接口。(这并不是一个函数式接口。)

获取一个流非常简单,有以下几种常用的方式:

  • 所有的Collection集合都可以通过stream默认方法获取流;
  • Stream接口的静态方法of可以获取数组对应的流。

方式1 : 根据Collection获取流

首先,java.util.Collection接口中加入了default方法stream用来获取流,所以其所有实现类均可获取流。

import java.util.*;
import java.util.stream.Stream;
/*
    获取Stream流的方式

    1.Collection中 方法
        Stream stream()
    2.Stream接口 中静态方法
        of(T...t) 向Stream中添加多个数据
 */
public class Demo13GetStream {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        // ...
        Stream<String> stream1 = list.stream();

        Set<String> set = new HashSet<>();
        // ...
        Stream<String> stream2 = set.stream();
    }
}

方式2: 根据数组获取流

如果使用的不是集合或映射而是数组,由于数组对象不可能添加默认方法,所以Stream接口中提供了静态方法of,使用很简单:

import java.util.stream.Stream;

public class Demo14GetStream {
    public static void main(String[] args) {
        String[] array = { "张无忌", "张翠山", "张三丰", "张一元" };
        Stream<String> stream = Stream.of(array);
    }
}

备注:of方法的参数其实是一个可变参数,所以支持数组。

4 常用方法

流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种:

  • 终结方法:返回值类型不再是Stream接口自身类型的方法,因此不再支持类似StringBuilder那样的链式调用。本小节中,终结方法包括countforEach方法。
  • 非终结方法:返回值类型仍然是Stream接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方法均为非终结方法。)

备注:本小节之外的更多方法,请自行参考API文档。

forEach : 逐一处理

虽然方法名字叫forEach,但是与for循环中的“for-each”昵称不同,该方法并不保证元素的逐一消费动作在流中是被有序执行的

void forEach(Consumer<? super T> action);
参数:
	Consumer属于消费函数式接口,我们在使用void forEach(Consumer<? super T> action);方法的时候可以传递lambda,完成Consumer接口中的 void accept(T t);

该方法接收一个Consumer接口函数,会将每一个流元素交给该函数进行处理。例如:

import java.util.stream.Stream;

public class Demo15StreamForEach {
    public static void main(String[] args) {
        Stream<String> stream =  Stream.of("大娃","二娃","三娃","四娃","五娃","六娃","七娃","爷爷","蛇精","蝎子精");
        //Stream<String> stream = Stream.of("张无忌", "张三丰", "周芷若");
        stream.forEach((String str)->{System.out.println(str);});
    }
}

在这里,lambda表达式(String str)->{System.out.println(str);}就是一个Consumer函数式接口的示例。

filter:过滤

可以通过filter方法将一个流转换成另一个子集流。方法声明:

Stream<T> filter(Predicate<? super T> predicate);
参数:
	Predicate 表示判断函数式接口,可以使用lambda,我们在使用filter(Predicate<? super T> predicate);方法的时候可以传递lambda完成Predicate 接口中的抽象方法 boolean test(T t);
	如果,满足条件,方法test(T t)返回true,那么filter(Predicate<? super T> predicate);就会将满足条件的数据放到流水线中

该接口接收一个Predicate函数式接口参数(可以是一个Lambda)作为筛选条件。

基本使用

Stream流中的filter方法基本使用的代码如:

public class Demo16StreamFilter {
    public static void main(String[] args) {
        Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
        Stream<String> result = original.filter((String s) -> {return s.startsWith("张");});
    }
}

在这里通过Lambda表达式来指定了筛选的条件:必须姓张。

count:统计个数

正如旧集合Collection当中的size方法一样,流提供count方法来数一数其中的元素个数:

long count();

该方法返回一个long值代表元素个数(不再像旧集合那样是int值)。基本使用:

public class Demo17StreamCount {
    public static void main(String[] args) {
        Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
        Stream<String> result = original.filter(s -> s.startsWith("张"));
        System.out.println(result.count()); // 2
    }
}

limit:取用前几个

limit方法可以对流进行截取,只取用前n个。方法签名:

Stream<T> limit(long maxSize):获取Stream流对象中的前n个元素,返回一个新的Stream流对象

参数是一个long型,如果流水线当前长度大于参数则进行截取;否则不进行操作。基本使用:

import java.util.stream.Stream;

public class Demo18StreamLimit {
    public static void main(String[] args) {
        Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
        Stream<String> result = original.limit(2);
        System.out.println(result.count()); // 2
    }
}

skip:跳过前几个

如果希望跳过前几个元素,可以使用skip方法获取一个截取之后的新流:

Stream<T> skip(long n): 跳过Stream流对象中的前n个元素,返回一个新的Stream流对象

如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。基本使用:

import java.util.stream.Stream;

public class Demo19StreamSkip {
    public static void main(String[] args) {
        Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
        Stream<String> result = original.skip(2);
        System.out.println(result.count()); // 1
    }
}

映射:map

如果需要将流中的元素映射到另一个流中,可以使用map方法。方法说明:

<R> Stream<R> map(Function<? super T, ? extends R> mapper);
属于延迟方法

该接口需要一个Function函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。

复习Function<T,R>接口

此前我们已经学习过java.util.stream.Function函数式接口,其中唯一的抽象方法为:

R apply(T t);

这可以将一种T类型转换成为R类型,而这种转换的动作,就称为“映射”。

对于map方法的解释举例,如下所示:

基本使用

Stream流中的map方法基本使用的代码如:

public class StreamMapDemo {
    public static void main(String[] args) {
        //创建老流对象存储String类型
        Stream<String> strStream = Stream.of("123", "345", "567");
        //使用map方法将老流转换为新流
        //<R> Stream<R> map(Function<? super T, ? extends R> mapper);
        //由于map方法的参数需要Function接口,Function只有一个抽象方法R apply(T t);属于函数式接口
        //可以使用lambda
        /*Stream<Integer> integerStream = strStream.map((s) -> {
            return Integer.parseInt(s);
        });*/
		 Stream<Integer> integerStream = strStream.map(s->Integer.parseInt(s));
      
        //取出新流中的数据
        integerStream.forEach(name->System.out.println(name));
    }
}

这段代码中,map方法的参数通过方法引用,将字符串类型转换成为了int类型(并自动装箱为Integer类对象)。

concat:组合

如果有两个流,希望合并成为一个流,那么可以使用Stream接口的静态方法concat

static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b): 把参数列表中的两个Stream流对象a和b,合并成一个新的Stream流对象

备注:这是一个静态方法,与java.lang.String当中的concat方法是不同的。

该方法的基本使用代码如:

import java.util.stream.Stream;

public class Demo20StreamConcat {
    public static void main(String[] args) {
        Stream<String> streamA = Stream.of("张无忌");
        Stream<String> streamB = Stream.of("张翠山");
        Stream<String> result = Stream.concat(streamA, streamB);
    }
}

5 Stream综合案例

现在有两个ArrayList集合存储队伍当中的多个成员姓名,要求使用Stream依次进行以下若干操作步骤:

  1. 第一个队伍只要名字为3个字的成员姓名;
  2. 第一个队伍筛选之后只要前3个人;
  3. 第二个队伍只要姓张的成员姓名;
  4. 第二个队伍筛选之后不要前2个人;
  5. 将两个队伍合并为一个队伍;
  6. 根据姓名创建Person对象;
  7. 打印整个队伍的Person对象信息。

两个队伍(集合)的代码如下:

public class DemoArrayListNames {
    public static void main(String[] args) {
        List<String> one = new ArrayList<>();
        one.add("迪丽热巴");
        one.add("宋远桥");
        one.add("苏星河");
        one.add("老子");
        one.add("庄子");
        one.add("孙子");
        one.add("洪七公");

        List<String> two = new ArrayList<>();
        two.add("古力娜扎");
        two.add("张无忌");
        two.add("张三丰");
        two.add("赵丽颖");
        two.add("张二狗");
        two.add("张天爱");
        two.add("张三");
		// ....
    }
}

Person类的代码为:

public class Person {
    
    private String name;

    public Person() {}

    public Person(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "'}";
    }

    public String getName() {
        return name;
    }

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

Stream方式

Stream流式处理代码为:

public class DemoStreamNames {
    public static void main(String[] args) {
        List<String> one = new ArrayList<>();
        // ...

        List<String> two = new ArrayList<>();
        // ...

        // 第一个队伍只要名字为3个字的成员姓名;
        // 第一个队伍筛选之后只要前3个人;
        Stream<String> streamOne = one.stream().filter(s -> s.length() == 3).limit(3);

        // 第二个队伍只要姓张的成员姓名;
        // 第二个队伍筛选之后不要前2个人;
        Stream<String> streamTwo = two.stream().filter(s -> s.startsWith("张")).skip(2);

        // 将两个队伍合并为一个队伍;
        // 根据姓名创建Person对象;
        // 打印整个队伍的Person对象信息。
         Stream.concat(streamOne, streamTwo).map((name)->{return new Person(name);}).forEach((p)->{System.out.println(p);});
    }
}

运行效果:

Person{name='宋远桥'}
Person{name='苏星河'}
Person{name='洪七公'}
Person{name='张二狗'}
Person{name='张天爱'}
Person{name='张三'}

6 收集Stream结果

对流操作完成之后,如果需要将其结果进行收集,例如获取对应的集合、数组等,如何操作?

收集到集合中

Stream流提供collect方法,其参数需要一个java.util.stream.Collector<T,A, R>接口对象来指定收集到哪种集合中。幸运的是,java.util.stream.Collectors类提供一些方法,可以作为Collector接口的实例:

  • public static <T> Collector<T, ?, List<T>> toList():转换为List集合。
  • public static <T> Collector<T, ?, Set<T>> toSet():转换为Set集合。

下面是这两个方法的基本使用代码:

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Demo15StreamCollect {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("10", "20", "30", "40", "50");
        List<String> list = stream.collect(Collectors.toList());
        Set<String> set = stream.collect(Collectors.toSet());
    }
}

收集到数组中

Stream提供toArray方法来将结果放到一个数组中,返回值类型是Object[]的:

Object[] toArray();

其使用场景如:

import java.util.stream.Stream;

public class Demo16StreamArray {
    public static void main(String[] args) {
        Stream<String> stream = Stream.of("10", "20", "30", "40", "50");
        Object[] objArray = stream.toArray();
    }
}
posted @ 2023-02-15 16:19  忘了鱼尾纱的猫  阅读(24)  评论(0)    收藏  举报