JAVA(高级应用篇)

异常

什么是异常

指的是程序在执行过程中,出现的非正常情况,如果不处理最终会导致 JVM 的非 正常停止。

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

代码逻辑错误,只是没有得到想要的结果,例如:求 a 与 b 的和,你 写成了 a-b

 如何对待异常

对于程序出现的异常,一般有两种解决方法:一是遇到错误就终止程序的运 行。另一种方法是程序员在编写程序时,就充分考虑到各种可能发生的异常和 错误,极力预防和避免。实在无法避免的,要编写相应的代码进行异常的检 测、以及异常的处理,保证代码的健壮性。

 Java 异常体系

Throwable

Throwable 中的常用方法:

• public void printStackTrace():打印异常的详细信息。 包含了异常的类型、异常的原因、异常出现的位置、在开发和调试阶段都得使用 printStackTrace。

• public String getMessage():获取发生异常的原因。

Error 和 Exception 

Throwable 可分为两类:Error 和 Exception。分别对应着 java.lang.Error 与 java.lang.Exception 两个类

Error:Java 虚拟机无法解决的严重问题。如:JVM 系统内部错误、资源耗尽等 严重情况。一般不编写针对性的代码进行处理。

例如:StackOverflowError(栈内存溢出)和 OutOfMemoryError(堆内存溢出,简称 OOM)。

 Exception: 其它因编程错误或偶然的外在因素导致的一般性问题,需要使用针 对性的代码进行处理,使程序继续运行。否则一旦发生异常,程序也会挂掉。

例如:空指针访问,试图读取不存在的文件,网络连接中断,数组角标越界等

无论是 Error 还是 Exception,还有很多子类,异常的类型非常丰富。 当代码运行出现异常时,特别是我们不熟悉的异常时,不要紧张,把 异常的简单类名,拷贝到 API 中去查去认识它即可。 

异常处理,其实针对的就是 Exception。 

编译时异常和运行时异常

Java 程序的执行分为编译时过程和运行时过程。有的错误只有在运行时才会发 生。比如:除数为 0,数组下标越界等

根据异常可能出现的阶段,可以将异常分为:

        编译时期异常(即 checked 异常、受检异常):在代码编译阶段,编译器就能明确警 示当前代码可能发生(不是一定发生)xx 异常,并明确督促程序员提前编写处理它 的代码。如果程序员没有编写对应的异常处理代码,则编译器就会直接判定编译失 败,从而不能生成字节码文件。通常,这类异常的发生不是由程序员的代码引起的, 或者不是靠加简单判断就可以避免的,例如:FileNotFoundException(文件找不到异 常)

        运行时期异常(即 runtime 异常、unchecked 异常、非受检异常):在代码编译阶段, 编译器完全不做任何检查,无论该异常是否会发生,编译器都不给出任何提示。只有 等代码运行起来并确实发生了 xx 异常,它才能被发现。通常,这类异常是由程序员 的代码编写不当引起的,只要稍加判断,或者细心检查就可以避免。

         – java.lang.RuntimeException:类及它的子类都是运行时异常。比如: ArrayIndexOutOfBoundsException:数组下标越界异常,ClassCastException 类型转换异常。 

常见的错误和异常 

Error

OutOfMemoryError(内存溢出错误)
    @Test
    public void test02(){
        //OutOfMemoryError
        //方式一:
        int[] arr = new int[Integer.MAX_VALUE];
    }
    @Test
    public void test03(){
        //OutOfMemoryError
        //方式二:
        StringBuilder s = new StringBuilder();
        while(true){
            s.append("atguigu");
        }
    }

StackOverflowError(内存错误)
 @Test
    public void test01(){
        //StackOverflowError
        recursion();
    }
    public void recursion(){ //递归方法
        recursion();
    }

运行时异常 

NullPointerException(空指针异常)
   @Test
    public void test01(){
        //NullPointerException
        int[][] arr = new int[3][];
        System.out.println(arr[0].length);
    }
ClassCastException(类转换异常)
   @Test
    public void test02(){
        //ClassCastException
        Object obj = 15;
        String str = (String) obj;
    }
ArrayIndexOutOfBoundsException(数组越界异常)
   @Test
    public void test03(){
        //ArrayIndexOutOfBoundsException
        int[] arr = new int[5];
        for (int i = 1; i <= 5; i++) {
            System.out.println(arr[i]);
        }
    }
 InputMisMatchException(输入类型不匹配异常)
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        System.out.print("请输入一个整数:");//输入非整数
        int num = input.nextInt();
        input.close();
    }
ArithmeticException(算数异常)
   @Test
    public void test05() {
        int a = 1;
        int b = 0;
        //ArithmeticException
        System.out.println(a / b);
    }

编译时异常

  @Test
    public void test06() throws InterruptedException {
        Thread.sleep(1000);//休眠 1 秒 InterruptedException
    }
    @Test
    public void test07() throws ClassNotFoundException {
        Class c = Class.forName("java.lang.String");//ClassNotFoundException
    }
    @Test
    public void test08() throws SQLException {
        Connection conn = DriverManager.getConnection("...."); //SQLException
    }
    @Test
    public void test09() throws FileNotFoundException {
        FileInputStream fis = new FileInputStream(" Java .txt "); //FileNotFoundException
    }
    @Test
    public void test10() throws IOException {
        //创建一个文件对象
        File file = new File(" Java .txt");
        //创建一个文件输入流对象
        FileInputStream fis = new FileInputStream(file);//FileNotFoundException
        //读取文件中的一个字节,并将其转换为整数
        int b = fis.read();//IOException
        //循环读取文件中的字节,直到读取到-1为止
        while(b != -1){
            System.out.print((char)b);
            b = fis.read();//IOException
        }
        //关闭文件输入流
        fis.close();//IOException
    }

异常的处理

方式一:try-catch-finally

方式二:throws + 异常类型

方式 1:捕获异常(try-catch-finally)

Java 提供了异常处理的抓抛模型。

• 前面提到,Java 程序的执行过程中如出现异常,会生成一个异常类对象,该异常对象 将被提交给 Java 运行时系统,这个过程称为抛出(throw)异常。

• 如果一个方法内抛出异常,该异常对象会被抛给调用者方法中处理。如果异常没有在 调用者方法中处理,它继续被抛给这个调用方法的上层方法。这个过程将一直继续下 去,直到异常被处理。这一过程称为捕获(catch)异常。

• 如果一个异常回到 main()方法,并且 main()也不处理,则程序运行终止。

try-catch-finally 基本格式 

try{
...... //可能产生异常的代码
}
catch( 异常类型 1 e ){
...... //当产生异常类型 1 型异常时的处置措施
}
catch( 异常类型 2 e ){
...... //当产生异常类型 2 型异常时的处置措施
} 
finally{
...... //无论是否发生异常,都无条件执行的语句
}

整体执行过程:

当某段代码可能发生异常,不管这个异常是编译时异常(受检异常)还是运行 时异常(非受检异常),我们都可以使用 try 块将它括起来,并在 try 块下面编 写 catch 分支尝试捕获对应的异常对象。

        如果在程序运行时,try 块中的代码没有发生异常,那么 catch 所有的分支都不执行

        如果在程序运行时,try 块中的代码发生了异常,根据异常对象的类型,将从上到下 选择第一个匹配的 catch 分支执行。此时 try 中发生异常的语句下面的代码将不执 行,而整个 try...catch 之后的代码可以继续运行。

        如果在程序运行时,try 块中的代码发生了异常,但是所有 catch 分支都无法匹配(捕 获)这个异常,那么 JVM 将会终止当前方法的执行,并把异常对象“抛”给调用者。如 果调用者不处理,程序就挂了。

try

• 捕获异常的第一步是用 try{…}语句块选定捕获异常的范围,将可能出现异常的业务 逻辑代码放在 try 语句块中 

catch (Exceptiontype e)

• catch 分支,分为两个部分,catch()中编写异常类型和异常参数名,{}中编写如果发生 了这个异常,要做什么处理的代码。

• 如果明确知道产生的是何种异常,可以用该异常类作为 catch 的参数;也可以用其父 类作为 catch 的参数。 比如:可以用 ArithmeticException 类作为参数的地方,就可以用 RuntimeException 类作为参数,或者用所有异常的父类 Exception 类作为参数。但不能是与 ArithmeticException 类无关的异常,如 NullPointerException(catch 中的语句将不 会执行)。

• 每个 try 语句块可以伴随一个或多个 catch 语句,用于处理可能产生的不同类型的异 常对象。

• 如果有多个 catch 分支,并且多个异常类型有父子类关系,必须保证小的子异常类型 在上,大的父异常类型在下。否则,报错。

• catch 中常用异常处理的方式

– public String getMessage():获取异常的描述信息,返回字符串

– public void printStackTrace():打印异常的跟踪栈信息并输出到 控制台。包含了异常的类型、异常的原因、还包括异常出现的位置,在开 发和调试阶段,都得使用 printStackTrace()。

举例

  @Test
           public void test() {

       int friends[] = {1, 2, 3, 4};
       try {
           for (int i = 0; i < 5; i++) {
               System.out.println(friends[i]);
           }
       } catch (ArrayIndexOutOfBoundsException e) {
           System.out.println("index err");
       }
       System.out.println("\nthis is the end");

   }

 

举例二

    @Test
    public void test1(){
        try{
            String str1 = "ass";
            str1 = null;
            System.out.println(str1.charAt(0));
        }catch(NullPointerException e){
//异常的处理方式 1
            System.out.println("不好意思,亲~出现了小问题,正在加紧解决...");
        }catch(ClassCastException e){
//异常的处理方式 2
            System.out.println("出现了类型转换的异常");
        }catch(RuntimeException e){
//异常的处理方式 3
            System.out.println("出现了运行时异常");
        }
//此处的代码,在异常被处理了以后,是可以正常执行的
        System.out.println("hello");
    }

finally 使用及举例

因为异常会引发程序跳转,从而会导致有些语句执行不到。而程序中有一些特定的代 码无论异常是否发生,都需要执行。例如,数据库连接、输入流输出流、Socket 连 接、Lock 锁的关闭等,这样的代码通常就会放到 finally 块中。所以,我们通常将一定 要被执行的代码声明在 finally 中。

                – 唯一的例外,使用 System.exit(0) 来终止当前正在运行的 Java 虚拟机。

不论在 try 代码块中是否发生了异常事件,catch 语句是否执行,catch 语句是否有异 常,catch 语句中是否有 return,finally 块中的语句都会被执行。

finally 语句和 catch 语句是可选的,但 finally 不能单独使用。 

try{

}finally{

}

 举例一

@Test
 public void test1(){
 FileInputStream fis = null;
 try{
 File file = new File("hello1.txt");
 fis = new FileInputStream(file);//FileNotFoundException
 int b = fis.read();//IOException
 while(b != -1){
 System.out.print((char)b);
 b = fis.read();//IOException
 }
 }catch(IOException e){
 e.printStackTrace();
 }finally{
 try {
 if(fis != null)
 fis.close();//IOException
 } catch (IOException e) {
 e.printStackTrace();
 }
 }
 }
}

 举例二

public static void main(String[] args) {
 int result = test("12");
 System.out.println(result);
 }
 public static int test(String str){
 try{
 Integer.parseInt(str);
 return 1;
 }catch(NumberFormatException e){
 return -1;
 }finally{
 System.out.println("test 结束");
 }
 }

异常处理的体会

前面使用的异常都是 RuntimeException 类或是它的子类,这些类的异常的特点 是:即使没有使用 try 和 catch 捕获,Java 自己也能捕获,并且编译通过 ( 但运行时 会发生异常使得程序运行终止 )。所以,对于这类异常,可以不作处理,因为这类异 常很普遍,若全处理可能会对程序的可读性和运行效率产生影响。

如果抛出的异常是 IOException 等类型的非运行时异常,则必须捕获,否则编译错 误。也就是说,我们必须处理编译时异常,将异常进行捕捉,转化为运行时异常。

方式 2:声明抛出异常类型(throws) 

如果在编写方法体的代码时,某句代码可能发生某个编译时异常,不处理编译不通 过,但是在当前方法体中可能不适合处理或无法给出合理的处理方式,则此方法应 显示地声明抛出异常,表明该方法将不对这些异常进行处理,而由该方法的调用者负 责处理。

具体方式:在方法声明中用 throws 语句可以声明抛出异常的列表,throws 后面的异 常类型可以是方法中产生的异常类型,也可以是它的父类。 

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

在 throws 后面可以写多个异常类型,用逗号隔开。 

public void readFile(String file) throws FileNotFoundException,IOExc
eption {
...
// 读文件的操作可能产生 FileNotFoundException 或 IOException 类型的异
常
FileInputStream fis = new FileInputStream(file);
 //...
}

 举例

针对于编译时异常

public static void main(String[] args) {
        System.out.println("上课.....");
        try {
            afterClass();//换到这里处理异常
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("准备提前上课");
        }
        System.out.println("上课.....");
    }
    public static void afterClass() throws InterruptedException {
        for(int i=10; i>=1; i--){
            Thread.sleep(1000);//本来应该在这里处理异常
            System.out.println("距离上课还有:" + i + "分钟");
        }
    }

针对于运行时异常:

throws 后面也可以写运行时异常类型,只是运行时异常类型,写或不写对于编 译器和程序执行来说都没有任何区别。如果写了,唯一的区别就是调用者调用 该方法后,使用 try...catch 结构时,IDEA 可以获得更多的信息,需要添加哪种 catch 分支。

   public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        try {
            System.out.print("请输入第一个整数:");
            int a = input.nextInt();
            System.out.print("请输入第二个整数:");
            int b = input.nextInt();
            int result = divide(a,b);
            System.out.println(a + "/" + b +"=" + result);
        } catch (InputMismatchException e) {
            System.out.println("输入错误,请输入整数");
            e.printStackTrace();
        }



    }
    public static int divide(int a, int b) throws  ArithmeticException{
        return a/b;
    }

方法重写中 throws 的要求 

如果父类被重写方法的方法签名后面没有 “throws 编译时异常类型”,那么重写方法 时,方法签名后面也不能出现“throws 编译时异常类型”。

如果父类被重写方法的方法签名后面有 “throws 编译时异常类型”,那么重写方法 时,throws 的编译时异常类型必须 <= 被重写方法 throws 的编译时异常类型,或者 不 throws 编译时异常。

方法重写,对于“throws 运行时异常类型”没有要求。

class Father{
 public void method()throws Exception{
 System.out.println("Father.method");
 }
}
class Son extends Father{
 @Override
 public void method() throws IOException,ClassCastException {
 System.out.println("Son.method");
 }

 两种异常处理方式的选择

如果程序代码中,涉及到资源的调用(流、数据库连接、网络连接等),则必须考虑 使用 try-catch-finally 来处理,保证不出现内存泄漏。

如果父类被重写的方法没有 throws 异常类型,则子类重写的方法中如果出现异常, 只能考虑使用 try-catch-finally 进行处理,不能 throws

开发中,方法 a 中依次调用了方法 b,c,d 等方法,方法 b,c,d 之间是递进关系。此时, 如果方法 b,c,d 中有异常,我们通常选择使用 throws,而方法 a 中通常选择使用 trycatch-finally。

手动抛出异常对象:throw 

Java 中异常对象的生成有两种方式:

由虚拟机自动生成:程序运行过程中,虚拟机检测到程序发生了问题,那么针对当前 代码,就会在后台自动创建一个对应异常类的实例对象并抛出。

由开发人员手动创建:new 异常类型([实参列表]);,如果创建好的异常对象不抛 出对程序没有任何影响,和创建一个普通对象一样,但是一旦 throw 抛出,就会对程 序运行产生影响了。

使用格式

throw new 异常类名(参数);

无论是编译时异常类型的对象,还是运行时异常类型的对象,如果没有被 try..catch 合理的处理,都会导致程序崩溃。

throw 语句会导致程序执行流程被改变,throw 语句是明确抛出一个异常对象, 因此它下面的代码将不会执行。

如果当前方法没有 try...catch 处理这个异常对象,throw 语句就会代替 return 语 句提前终止当前方法的执行,并返回一个异常对象给调用者。

 public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        try {
            System.out.print("请输入第一个整数:");
            int a = input.nextInt();
            System.out.print("请输入第二个整数:");
            int b = input.nextInt();
            int result = divide(a,b);
            System.out.println(a + "/" + b +"=" + result);
        } catch (InputMismatchException e) {
            System.out.println("输入错误,请输入整数");
          throw new RuntimeException("出异常了");
        }



    }
    public static int divide(int a, int b) throws  ArithmeticException{
        return a/b;
    }

 自定义异常

如何自定义异常类

(1)要继承一个异常类型

自定义一个编译时异常类型:自定义类继承 java.lang.Exception。

自定义一个运行时异常类型:自定义类继承 java.lang.RuntimeException。

(2)建议大家提供至少两个构造器,一个是无参构造,一个是(String message)构造器。 (3)自定义异常需要提供 serialVersionUID

注意点

1. 自定义的异常只能通过 throw 抛出。

2. 自定义异常最重要的是异常类的名字和 message 属性。当异常出现时,可以根据名字 判断异常类型。比如:TeamException("成员已满,无法添加");、 TeamException("该员工已是某团队成员");

3. 自定义异常对象只能手动抛出。抛出后由 try..catch 处理,也可以甩锅 throws 给调用 者处理。

 举例

自定义异常

public class MyException extends Exception{
    static final long serialVersionUID = 23423423435L;
    private int idnumber;
    public MyException(String message, int id) {
        super(message);
        this.idnumber = id;
    }
    public int getId() {
        return idnumber;
    }
}
public class MyExpTest {
    public void regist(int num) throws MyException {
        if (num < 0){
            //抛出自定义异常
            throw new MyException("人数为负值,不合理", 3);
        }

        else
            System.out.println("登记人数" + num);
    }
    public void manager() {
        try {
            regist(-1);
        } catch (MyException e) {
            System.out.println("登记失败,出错种类" + e.getId());
        }
        System.out.print("本次登记操作结束");
    }
    public static void main(String args[]) {
        MyExpTest t = new MyExpTest();
        t.manager();
    }

}

小结

世界上最遥远的距离,是我在 if 里你在 else 里,似乎一直相伴又永远分离;

世界上最痴心的等待,是我当 case 你是 switch,或许永远都选不上自己;

世界上最真情的相依,是你在 try 我在 catch。无论你发神马脾气,我都默默承 受,静静处理。到那时,再来期待我们的 finally。 

多线程 

程序、进程与线程

程序(program):为完成特定任务,用某种语言编写的一组指令的集合。即指一段 静态的代码,静态对象。

进程(process):程序的一次执行过程,或是正在内存中运行的应用程序。如:运行 中的 QQ,运行中的网易音乐播放器。

        – 每个进程都有一个独立的内存空间,系统运行一个程序即是一个进程从创 建、运行到消亡的过程。(生命周期)

        – 程序是静态的,进程是动态的 – 进程作为操作系统调度和分配资源的最小单位(亦是系统运行程序的基 本单位),系统在运行时会为每个进程分配不同的内存区域。

        – 现代的操作系统,大都是支持多进程的,支持同时运行多个程序。比如: 现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板, dos 窗口等软件。

线程(thread):进程可进一步细化为线程,是程序内部的一条执行路径。一个进程 中至少有一个线程。

        – 一个进程同一时间若并行执行多个线程,就是支持多线程的

        – 线程作为 CPU 调度和执行的最小单位。

        – 一个进程中的多个线程共享相同的内存单元,它们从同一个堆中分配对 象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但 多个线程操作共享的系统资源可能就会带来安全的隐患。

多线程程序的优点

1. 提高应用程序的响应。对图形化界面更有意义,可增强用户体验。

2. 提高计算机系统 CPU 的利用率

3. 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改

单核 CPU 和多核 CPU

单核 CPU,在一个时间单元内,只能执行一个线程的任务。例如,可以把 CPU 看成是医院的医生诊室,在一定时间内只能给一个病人诊断治疗。所以单核 CPU 就是,代码经过前面一系列的前导操作(类似于医院挂号,比如有 10 个 窗口挂号),然后到 cpu 处执行时发现,就只有一个 CPU(对应一个医生), 大家排队执行。

这时候想要提升系统性能,只有两个办法,要么提升 CPU 性能(让医生看病快 点),要么多加几个 CPU(多整几个医生),即为多核的 CPU。

一个是多个核心的其他共用资源限制。譬如,4 核 CPU 对应的内存、cache、寄存 器并没有同步扩充 4 倍。这就好像医院一样,1 个医生换 4 个医生,但是做 B 超检查 的还是一台机器,性能瓶颈就从医生转到 B 超检查了。

另一个是多核 CPU 之间的协调管理损耗。譬如多个核心同时运行两个相关的任务, 需要考虑任务同步,这也需要消耗额外性能。好比公司工作,一个人的时候至少不用 开会浪费时间,自己跟自己商量就行了。两个人就要开会同步工作,协调分配,所以 工作效率绝对不可能达到 2 倍

并行与并发 

并行(parallel):指两个或多个事件在同一时刻发生(同时发生)。指在同一时刻, 有多条指令在多个 CPU 上同时执行。比如:多个人同时做不同的事

并发(concurrency):指两个或多个事件在同一个时间段内发生。即在一段时间 内,有多条指令在单个 CPU 上快速轮换、交替执行,使得在宏观上具有多个进程同 时执行的效果

Java 语言的 JVM 允许程序运行多个线程,使用 java.lang.Thread 类代表线程,所 有的线程对象都必须是 Thread 类或其子类的实例。

Thread 类的特性

        – 每个线程都是通过某个特定 Thread 对象的 run()方法来完成操作的,因此 把 run()方法体称为线程执行体。

        – 通过该 Thread 对象的 start()方法来启动这个线程,而非直接调用 run()

        – 要想实现多线程,必须在主线程中创建新的线程对象.

 创建和启动线程

        方式 1:继承 Thread 类

1. 定义 Thread 类的子类,并重写该类的 run()方法,该 run()方法的方法体就代表了线程 需要完成的任务

2. 创建 Thread 子类的实例,即创建了线程对象

3. 调用线程对象的 start()方法来启动该线程

package streat;

public class test1 extends  Thread{
    @Override
    public void run() {
        for (int i=1;i<10;i++)
        {
            System.out.println(getName()+"正在执行"+i);
        }
    }
}
class main{
    public static void main(String[] args) {
        new test1().start();
        new test1().start();
    }
}

1.如果自己手动调用 run()方法,那么就只是普通方法,没有启动多线程模 式。

2.run()方法由 JVM 调用,什么时候调用,执行的过程控制都有操作系统的 CPU 调度决定

3.想要启动多线程,必须调用 start 方法

4.一个线程对象只能调用一次 start()方法启动,如果重复调用了,则将抛出 以上的异常“IllegalThreadStateException”。

方式 2:实现 Runnable 接口 

Java 有单继承的限制,当我们无法继承 Thread 类时,那么该如何做呢?在核心 类库中提供了 Runnable 接口,我们可以实现 Runnable 接口,重写 run()方法, 然后再通过 Thread 类的对象代理启动和执行我们的线程体 run()方法

2. 定义 Runnable 接口的实现类,并重写该接口的 run()方法,该 run()方法的方法体同 样是该线程的线程执行体。

3. 创建 Runnable 实现类的实例,并以此实例作为 Thread 的 target 参数来创建 Thread 对象,该 Thread 对象才是真正 的线程对象。

4. 调用线程对象的 start()方法,启动线程。调用 Runnable 接口实现类的 run 方法。 

public class test2 implements Runnable{
    @Override
    public void run() {
        for (int i=1;i<10;i++)
        {
            System.out.println(Thread.currentThread().getName()+"正在执行"+i);
        }
    }
}
class test{
    public static void main(String[] args) {
        test2 test2 = new test2();
        Thread thread = new Thread(test2,"场景");
        thread.start();
 
    }
}

 通过实现 Runnable 接口,使得该类有了多线程类的特征。所有的分线程要执 行的代码都在 run 方法里面。

在启动的多线程的时候,需要先通过 Thread 类的构造方法 Thread(Runnable target) 构造出对象,然后调用 Thread 对象的 start()方法来运行多线程代码。 实际上,所有的多线程代码都是通过运行 Thread 的 start()方法来运行的。因 此,不管是继承 Thread 类还是实现 Runnable 接口来实现多线程,最终还是通 过 Thread 的对象的 API 来控制线程的,熟悉 Thread 类的 API 是进行多线程编 程的基础。

说明:Runnable 对象仅仅作为 Thread 对象的 target,Runnable 实现类里包含 的 run()方法仅作为线程执行体。 而实际的线程对象依然是 Thread 实例,只是 该 Thread 线程负责执行其 target 的 run()方法。

变形写法 

使用匿名内部类对象来实现线程的创建和启动

new Thread("新的线程!"){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在执行!"+i);
}
}
}.start();
new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":" +
i);
}
}
}).start();

对比两种方式

继承 Thread:线程代码存放 Thread 子类 run 方法中。

 实现 Runnable:线程代码存在接口的子类的 run 方法。

实现 Runnable 接口比继承 Thread 类所具有的优势

• 避免了单继承的局限性

• 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资 源。

• 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。

构造器 

• public Thread() :分配一个新的线程对象。

• public Thread(String name) :分配一个指定名字的新的线程对象。

• public Thread(Runnable target) :指定创建线程的目标对象,它实现了 Runnable 接口 中的 run 方法

• public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指 定名字。

常用方法系列

public void run() :此线程要执行的任务在此处定义代码。

public void start() :导致此线程开始执行; Java 虚拟机调用此线程的 run 方法。

public String getName() :获取当前线程名称

public void setName(String name):设置该线程名称。

public static Thread currentThread() :返回对当前正在执行的线程对象的引用。在 Thread 子类中就是 this,通常用于主线程和 Runnable 实现类

public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时 停止执行)。

public static void yield():yield 只是让当前线程暂停一下,让系统的线程调度器重新 调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这 个不能保证,完全有可能的情况是,当某个线程调用了 yield 方法暂停之后,线程调 度器又将其调度出来重新执行。

void join() :等待该线程终止。

public final boolean isAlive():测试线程是否处于活动状态。如果线程已经启动且尚未 终止,则为活动状态。

• public final int getPriority() :返回线程优先级

 测试

    public static void main(String[] args) {
        Thread t = new Thread(){
            public void run(){
                System.out.println(getName() + "的优先级:" + getPriority());
            }
        };

        t.setPriority(Thread.MAX_PRIORITY);
        t.start();
        System.out.println(Thread.currentThread().getName() +"的优先 级:" + Thread.currentThread().getPriority());
    }

 案例:

• 声明一个匿名内部类继承 Thread 类,重写 run 方法,实现打印[1,100]之间的偶数, 要求每隔 1 秒打印 1 个偶数。

• 声明一个匿名内部类继承 Thread 类,重写 run 方法,实现打印[1,100]之间的奇数,

– 当打印到 5 时,让奇数线程暂停一下,再继续。

– 当打印到 5 时,让奇数线程停下来,让偶数线程执行完再打印。

package streat;

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

            Thread te = new Thread() {
                @Override
                public void run() {
                    for (int i = 2; i <= 100; i += 2) {
                        System.out.println("偶数线程:" + i);
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            };
            te.start();
            Thread to = new Thread() {
                @Override
                public void run() {
                    for (int i = 1; i <= 100; i += 2) {
                        System.out.println("奇数线程:" + i);
                        if (i == 5) {
        //Thread.yield();
                            try {
                                te.join();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            };
            to.start();
        }
    }



 守护线程

有一种线程,它是在后台运行的,它的任务是为其他线程提供服务的,这种线 程被称为“守护线程”。

JVM 的垃圾回收线程就是典型的守护线程。 守护线程有个特点,就是如果所有非守护线程都死亡,那么守护线程自动死 亡。形象理解:兔死狗烹,鸟尽弓藏

调用 setDaemon(true)方法可将指定线程设置为守护线程。必须在线程启动之前 设置,否则会报 IllegalThreadStateException 异常。

调用 isDaemon()可以判断线程是否是守护线程。

package streat;

public class a {
    public static void main(String[] args) {
        MyDaemon m = new MyDaemon();
        m.setDaemon(true);
        m.start();
        for (int i = 1; i <= 100; i++) {
            System.out.println("main:" + i);
        }
    }
}
class MyDaemon extends Thread {
    public void run() {
        while (true) {
            System.out.println("我一直守护者你...");
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

多线程的生命周期

Java 语言使用 Thread 类及其子类的对象来表示线程,在它的一个完整的生命周 期中通常要经历如下一些状态:

public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}

• NEW(新建):线程刚被创建,但是并未启动。还没调用 start 方法。

• RUNNABLE(可运行):这里没有区分就绪和运行状态。因为对于 Java 对象来说,只 能标记为可运行,至于什么时候运行,不是 JVM 来控制的了,是 OS 来进行调度的, 而且时间非常短暂,因此对于 Java 对象的状态来说,无法区分。

• Teminated(被终止):表明此线程已经结束生命周期,终止运行。

• 重点说明,根据 Thread.State 的定义,阻塞状态分为三种:

BLOCKED、WAITING、 TIMED_WAITING。

– BLOCKED(锁阻塞):在 API 中的介绍为:一个正在阻塞、等待一个监视 器锁(锁对象)的线程处于这一状态。只有获得锁对象的线程才能有执行 机会。

• 比如,线程 A 与线程 B 代码中使用同一锁,如果线程 A 获取到 锁,线程 A 进入到 Runnable 状态,那么线程 B 就进入到 Blocked 锁阻塞状态。 – TIMED_WAITING(计时等待):在 API 中的介绍为:一个正在限时等待 另一个线程执行一个(唤醒)动作的线程处于这一状态。

• 当前线程执行过程中遇到 Thread 类的 sleep 或 join,Object 类 的 wait,LockSupport 类的 park 方法,并且在调用这些方法时, 设置了时间,那么当前线程会进入 TIMED_WAITING,直到时间 到,或被中断。

– WAITING(无限等待):在 API 中介绍为:一个正在无限期等待另一个线 程执行一个特别的(唤醒)动作的线程处于这一状态。

• 当前线程执行过程中遇到遇到 Object 类的 wait,Thread 类的 join,LockSupport 类的 park 方法,并且在调用这些方法时,没 有指定时间,那么当前线程会进入 WAITING 状态,直到被唤醒。

– 通过 Object 类的 wait 进入 WAITING 状态的要有 Object 的 notify/notifyAll 唤醒;

– 通过 Condition 的 await 进入 WAITING 状态的要有 Condition 的 signal 方法唤醒;

– 通过 LockSupport 类的 park 方法进入 WAITING 状态的要有 LockSupport 类的 unpark 方法唤醒

– 通过 Thread 类的 join 进入 WAITING 状态,只有调用 join 方法的线程对象结束才能让当前线程恢复;

线程安全问题及解决 

当我们使用多个线程访问同一资源(可以是同一个变量、同一个文件、同一条 记录等)的时候,若多个线程只有读操作,那么不会发生线程安全问题。但是如 果多个线程中对资源有读和写的操作,就容易出现线程安全问题。

举例

火车站要卖票,我们模拟火车站的卖票过程。因为疫情期间,本次列车的座位 共 100 个(即,只能出售 100 张火车票)。我们来模拟车站的售票窗口,实现 多个窗口同时售票的过程。注意:不能出现错票、重票。

package streat;

class Window extends Thread {
    public void run() {
        int ticket = 100;
        while (ticket > 0) {
            System.out.println(getName() + "卖出一张票,票号:" + ticket);
            ticket--;
        }
    }
}
public class a {
    public static void main(String[] args) {
        Window w1 = new Window();
        Window w2 = new Window();
        Window w3 = new Window();
        w1.setName("窗口 1");
        w2.setName("窗口 2");
        w3.setName("窗口 3");
        w1.start();
        w2.start();
        w3.start();
    }
}

 

结果:发现卖出 300 张票。

问题:局部变量是每次调用方法都是独立的,那么每个线程的 run()的 ticket 是 独立的,不是共享数据。

解决方案:静态变量是共享的

package streat;

class TicketSaleThread extends Thread {
    private static int ticket = 100;
    public void run() {
        while (ticket > 0) {
            try {
                Thread.sleep(10);//加入这个,使得问题暴露的更明显
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(getName() + "卖出一张票,票号:" + ticket);
            ticket--;
        }
    }
}
public class a {
    public static void main(String[] args) {
        TicketSaleThread t1 = new TicketSaleThread();
        TicketSaleThread t2 = new TicketSaleThread();
        TicketSaleThread t3 = new TicketSaleThread();
        t1.setName("窗口 1");
        t2.setName("窗口 2");
        t3.setName("窗口 3");
        t1.start();
        t2.start();
        t3.start();
    }
}

结果:发现卖出近 100 张票

问题 1:但是有重复票或负数票问题。

原因:线程安全问题

问题 2:如果要考虑有两场电影,各卖 100 张票等

原因:TicketThread 类的静态变量,是所有 TicketThread 类的对象共享

同一个对象的实例变量共享

示例代码:多个 Thread 线程使用同一个 Runnable 对象

package streat;

class TicketSaleRunnable implements Runnable {
    private int ticket = 100;
    public void run() {
        while (ticket > 0) {
            try {
                Thread.sleep(10);//加入这个,使得问题暴露的更明显
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket);
            ticket--;
        }
    }
}
class ma{
    public static void main(String[] args) {
        TicketSaleRunnable tr = new TicketSaleRunnable();
        Thread t1 = new Thread(tr, "窗口一");
        Thread t2 = new Thread(tr, "窗口二");
        Thread t3 = new Thread(tr, "窗口三");
        t1.start();
        t2.start();
        t3.start();
    }

}



结果:发现卖出近 100 张票。

问题:但是有重复票或负数票问题。

原因:线程安全问题

抽取资源类,共享同一个资源对象

//1、编写资源类
class Ticket {
 private int ticket = 100;
 public void sale() {
 if (ticket > 0) {
 try {
 Thread.sleep(10);//加入这个,使得问题暴露的更明显
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 System.out.println(Thread.currentThread().getName() + "卖
出一张票,票号:" + ticket);
 ticket--;
 } else {
 throw new RuntimeException("没有票了");
 }
 }
 public int getTicket() {
 return ticket;
 }
}
public class SaleTicketDemo5 {
 public static void main(String[] args) {
 //2、创建资源对象
 Ticket ticket = new Ticket();
 //3、启动多个线程操作资源类的对象
 Thread t1 = new Thread("窗口一") {
 public void run() {
 while (true) {
 ticket.sale();
 }
 }
 };
 Thread t2 = new Thread("窗口二") {
 public void run() {
 while (true) {
 ticket.sale();
 }
 }
 };
 Thread t3 = new Thread(new Runnable() {
 public void run() {
 ticket.sale();
 }
 }, "窗口三");
 t1.start();
 t2.start();
 t3.start();
 }
}

结果:发现卖出近 100 张票。

问题:但是有重复票或负数票问题。

原因:线程安全问题

同步机制解决线程安全问题

要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在 票问题,Java 中提供了同步机制 (synchronized)来解决。

窗口 1 线程进入操作的时候,窗口 2 和窗口 3 线程只能在外等着,窗口 1 操作 结束,窗口 1 和窗口 2 和窗口 3 才有机会进入代码去执行。也就是说在某个线 程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后, 才能去抢夺 CPU 资源,完成对应的操作,保证了数据的同步性,解决了线程不 安全的现象。

为了保证每个线程都能正常执行原子操作,Java 引入了线程同步机制。注意:在 任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程 只能在外等着(BLOCKED)。 

同步机制解决线程安全问题的原理 

同步机制的原理,其实就相当于给某段代码加“锁”,任何线程想要执行这段代 码,都要先获得“锁”,我们称它为同步锁。因为 Java 对象在堆中的数据分为分 为对象头、实例变量、空白的填充。而对象头中包含:     

• Mark Word:记录了和当前对象有关的 GC、锁标记等信息。

• 指向类的指针:每一个对象需要记录它是由哪个类创建出来的。

• 数组长度(只有数组对象才有)   

哪个线程获得了“同步锁”对象之后,”同步锁“对象就会记录这个线程的 ID,这 样其他线程就只能等待了,除非这个线程”释放“了锁对象,其他线程才能重新 获得/占用”同步锁“对象。

 同步代码块和同步方法

同步代码块:synchronized 关键字可以用于某个区块前面,表示只对这个区块 的资源实行互斥访问。

格式:

synchronized(同步锁){

需要同步操作的代码

}

同步方法:synchronized 关键字直接修饰方法,表示同一时刻只有一个线程能 进入这个方法,其他线程在外面等着。

public synchronized void method(){

可能会产生线程安全问题的代码

同步锁机制 

对于并发工作,你需要某种方式来防 止两个任务访问相同的资源(其实就是共享资源竞争)。 防止这种冲突的方法 就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须 锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁 之时,另一个任务就可以锁定并使用它了。

 synchronized 的锁是什么 

同步锁对象可以是任意类型,但是必须保证竞争“同一个共享资源”的多个线程 必须使用同一个“同步锁对象”。

对于同步代码块来说,同步锁对象是由程序员手动指定的(很多时候也是指定 为 this 或类名.class),但是对于同步方法来说,同步锁对象只能是默认的:

• 静态方法:当前类的 Class 对象(类名.class)

• 非静态方法:this

 同步操作的思考顺序 

1、如何找问题,即代码是否存在线程安全?(非常重要) (1)明确哪些代码 是多线程运行的代码 (2)明确多个线程是否有共享数据 (3)明确多线程运 行代码中是否有多条语句操作共享数据

2、如何解决呢?(非常重要) 对多条操作共享数据的语句,只能让一个线程 都执行完,在执行过程中,其他线程不可以参与执行。 即所有操作共享数据的 这些语句都要放在同步范围中

3、切记: 范围太小:不能解决安全问题 范围太大:因为一旦某个线程抢到锁,其他线程就只能等待,所以范围太大, 效率会降低,不能合理利用 CPU 资源。

代码演示 

示例一:静态方法加锁

package streat;

public class Ticketstatic extends Thread{
    private static int ticket = 100;
    public void run(){//直接锁这里,肯定不行,会导致,只有一个窗口卖票
        while (ticket > 0) {
            saleOneTicket();
        }
    }
    public synchronized static void saleOneTicket(){//锁对象是 TicketSaleThread 类的 Class 对象,而一个类的 Class 对象在内存中肯定只有一个
        if(ticket > 0) {//不加条件,相当于条件判断没有进入锁管控,线程安全问题就没有解决
            System.out.println(Thread.currentThread().getName() + "卖 出一张票,票号:" + ticket);
            ticket--;
        }
    }
}
class staticTest{
    public static void main(String[] args) {
        Ticketstatic t1 = new Ticketstatic();
        Ticketstatic t2 = new Ticketstatic();
        Ticketstatic t3 = new Ticketstatic();
        t1.setName("窗口 1");
        t2.setName("窗口 2");
        t3.setName("窗口 3");
        t1.start();
        t2.start();
        t3.start();
    }
}

示例二:非静态方法加锁

package streat;

public class NoTicketstatic implements Runnable{
    private int ticket = 100;
    public void run() {//直接锁这里,肯定不行,会导致,只有一个窗口卖票
        while (ticket > 0) {
            saleOneTicket();
        }
    }
    public synchronized void saleOneTicket() {//锁对象是 this,NoTicketstatic 对象,因为上面 3 个线程使用同一个 NoTicketstatic 对象,所以可以
        if (ticket > 0) {//不加条件,相当于条件判断没有进入锁管控,线程安全问题就没有解决
            System.out.println(Thread.currentThread().getName() + "卖 出一张票,票号:" + ticket);
            ticket--;
        }
    }
}
class NoTicketstaticTest{
    public static void main(String[] args) {
        NoTicketstatic tr = new NoTicketstatic();
        Thread t1 = new Thread(tr, "窗口一");
        Thread t2 = new Thread(tr, "窗口二");
        Thread t3 = new Thread(tr, "窗口三");
        t1.start();
        t2.start();
        t3.start();
    }
    }

 示例三:同步代码块

public class SaleTicketDemo5 {
 public static void main(String[] args) {
 //2、创建资源对象
 Ticket ticket = new Ticket();
 //3、启动多个线程操作资源类的对象
 Thread t1 = new Thread("窗口一") {
 public void run() {//不能给 run()直接加锁,因为 t1,t2,t3 的三
个 run 方法分别属于三个 Thread 类对象,
 // run 方法是非静态方法,那么锁对象默认选 this,那么锁对象根本不是同一个
 while (true) {
 synchronized (ticket) {
 ticket.sale();
 }
 }
 }
 };
 Thread t2 = new Thread("窗口二") {
 public void run() {
 while (true) {
 synchronized (ticket) {
 ticket.sale();
 }
 }
 }
 };
 Thread t3 = new Thread(new Runnable() {
 public void run() {
 while (true) {
 synchronized (ticket) {
 ticket.sale();
 }
 }
 }
 }, "窗口三");
 t1.start();
 t2.start();
 t3.start();
 }
}
//1、编写资源类
class Ticket {
 private int ticket = 1000;
 public void sale() {//也可以直接给这个方法加锁,锁对象是 this,这里就是 Ticket 对象
 if (ticket > 0) {
 System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket);
 ticket--;
 } else {
 throw new RuntimeException("没有票了");
 }
 }
 public int getTicket() {
 return ticket;
 }
}

死锁

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要 的同步资源,就形成了线程的死锁

package streat;

public class DeadLockTest {
    public static void main(String[] args) {
        StringBuilder s1 = new StringBuilder();
        StringBuilder s2 = new StringBuilder();
        new Thread() {
            public void run() {
                synchronized (s1) {
                    s1.append("a");
                    s2.append("1");
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s2) {
                        s1.append("b");
                        s2.append("2");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();
        new Thread() {
            public void run() {
                synchronized (s2) {
                    s1.append("c");
                    s2.append("3");
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (s1) {
                        s1.append("d");
                        s2.append("4");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();


    }}

方式二

package streat;

public class A1 {
    public synchronized void foo(B b) {
        System.out.println("当前线程名: " + Thread.currentThread().getName()
                        + " 进入了 A 实例的 foo 方法"); // ①
        try {
            Thread.sleep(200);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        System.out.println("当前线程名: " + Thread.currentThread().getName()
                        + " 企图调用 B 实例的 last 方法"); // ③
        b.last();
    }
    public synchronized void last() {
        System.out.println("进入了 A 类的 last 方法内部");
    }
}
class B {
    public synchronized void bar(A1 a) {
        System.out.println("当前线程名: " + Thread.currentThread().getName()
                        + " 进入了 B 实例的 bar 方法"); // ②
        try {
            Thread.sleep(200);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        System.out.println("当前线程名: " + Thread.currentThread().getName()
                        + " 企图调用 A 实例的 last 方法"); // ④
        a.last();
    }
    public synchronized void last() {

        System.out.println("进入了 B 类的 last 方法内部");
    }
}
 class DeadLock implements Runnable {
    A1 a = new A1();
    B b = new B();
    public void init() {
        Thread.currentThread().setName("主线程");
// 调用 a 对象的 foo 方法
        a.foo(b);
        System.out.println("进入了主线程之后");
    }
    public void run() {
        Thread.currentThread().setName("副线程");
// 调用 b 对象的 bar 方法
        b.bar(a);
        System.out.println("进入了副线程之后");
    }
    public static void main(String[] args) {
        DeadLock dl = new DeadLock();
        new Thread(dl).start();
        dl.init();
    }
}

诱发死锁的原因:

• 互斥条件

• 占用且等待

• 不可抢夺(或不可抢占)

• 循环等待

以上 4 个条件,同时出现就会触发死锁。

解决死锁

死锁一旦出现,基本很难人为干预,只能尽量规避。可以考虑打破上面的诱发 条件。

针对条件 1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问 题。

针对条件 2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问 题。

针对条件 3:占用部分资源的线程在进一步申请其他资源时,如果申请不到, 就主动释放掉已经占用的资源。

针对条件 4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这 样避免循环等待问题。

Lock(锁)

与采用 synchronized 相比,Lock 可提供多种 锁方案,更灵活、更强大。Lock 通过显式定义同步锁对象来实现同步。同步锁使用 Lock 对象充当。

java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。锁提 供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问 共享资源之前应先获得 Lock 对象。

在实现线程安全的控制中,比较常用的是 ReentrantLock,可以显式加锁、释放 锁。

– ReentrantLock 类实现了 Lock 接口,它拥有与 synchronized 相同的并发 性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一 些特性。此外,它还提供了在激烈争用情况下更佳的性能。

• Lock 锁也称同步锁,加锁与释放锁方法,如下:

– public void lock() :加同步锁。

– public void unlock() :释放同步锁。

代码结构:

class A{
 //1. 创建 Lock 的实例,必须确保多个线程共享同一个 Lock 实例
private final ReentrantLock lock = new ReenTrantLock();
public void m(){
 //2. 调动 lock(),实现需共享的代码的锁定
lock.lock();
try{
//保证线程安全的代码;
}
finally{
 //3. 调用 unlock(),释放共享代码的锁定
lock.unlock(); 
}
}
}

 举例:

package streat;

import java.util.concurrent.locks.ReentrantLock;

class Window implements Runnable{
    int ticket = 100;
    //1. 创建 Lock 的实例,必须确保多个线程共享同一个 Lock 实例
    private final ReentrantLock lock = new ReentrantLock();
    public void run(){
        while(true){
            try{
                //2. 调动 lock(),实现需共享的代码的锁定
                lock.lock();
                if(ticket > 0){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(ticket--);
                }else{
                    break;
                }
            }finally{
                //3. 调用 unlock(),释放共享代码的锁定
                lock.unlock();
            }
        }
    }
}
 class ThreadLock {
    public static void main(String[] args) {
        Window t = new Window();
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        t1.start();
        t2.start();
    }
}

synchronized 与 Lock 的对比

1. Lock 是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized 是隐式锁,出了 作用域、遇到异常等自动解锁

2. Lock 只有代码块锁,synchronized 有代码块锁和方法锁

3. 使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性 (提供更多的子类),更体现面向对象。

4. (了解)Lock 锁可以对读不加锁,对写加锁,synchronized 不可以

5. (了解)Lock 锁可以有多种获取锁的方式,可以从 sleep 的线程中抢到锁, synchronized 不可以 说明:开发建议中处理线程安全问题优先使用顺序为:

Lock ----> 同步代码块 ----> 同步方法

线程的通信 

当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行,那 么多线程之间需要一些通信机制,可以协调它们的工作,以此实现多线程共同 操作一份数据。

比如:线程 A 用来生产包子的,线程 B 用来吃包子的,包子可以理解为同一资 源,线程 A 与线程 B 处理的动作,一个是生产,一个是消费,此时 B 线程必须 等到 A 线程完成后才能执行,那么线程 A 与线程 B 之间就需要线程通信,即— — 等待唤醒机制。

等待唤醒机制 

这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争 (race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。 在一个线程满足某个条件时,就进入等待状态(wait() / wait(time)), 等 待其他线程执行完他们的指定代码过后再将其唤醒(notify());或可以指定 wait 的时间,等时间到了自动唤醒;在有多个线程进行等待时,如果需要,可 以使用 notifyAll()来唤醒所有的等待线程。wait/notify 就是线程间的一种协 作机制

1. wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态是 WAITING 或 TIMED_WAITING。它 还要等着别的线程执行一个特别的动作,也即“通知(notify)”或者等待时间 到,在这个对象上等待的线程从 wait set 中释放出来,重新进入到调度队列 (ready queue)中

2. notify:则选取所通知对象的 wait set 中的一个线程释放;

3. notifyAll:则释放所通知对象的 wait set 上的全部线程。 注意: 被通知的线程被唤醒后也不一定能立即恢复执行,因为它当初中断的 地方是在同步块内,而此刻它已经不持有锁,所以它需要再次尝试去 获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。

总结如下: • 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE(可运行) 状态;

• 否则,线程就从 WAITING 状态又变成 BLOCKED(等待锁) 状态

调用 wait 和 notify 需注意的细节 

1. wait 方法与 notify 方法必须要由同一个锁对象调用。因为:对应的锁对象可以通 过 notify 唤醒使用同一个锁对象调用的 wait 方法后的线程。

2. wait 方法与 notify 方法是属于 Object 类的方法的。因为:锁对象可以是任意对 象,而任意对象的所属类都是继承了 Object 类的。

3. wait 方法与 notify 方法必须要在同步代码块或者是同步函数中使用。因为:必须 要通过锁对象调用这 2 个方法。否则会报 java.lang.IllegalMonitorStateException 异 常。

 举例

public class ConsumerProducerTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer p1 = new Producer(clerk);
Consumer c1 = new Consumer(clerk);
Consumer c2 = new Consumer(clerk);
p1.setName("生产者 1");
c1.setName("消费者 1");
c2.setName("消费者 2");
p1.start();
c1.start();
c2.start();
}
}
//生产者
class Producer extends Thread{
private Clerk clerk;
public Producer(Clerk clerk){
this.clerk = clerk;
}
@Override
public void run() {
System.out.println("=========生产者开始生产产品========");
while(true){
try {
Thread.sleep(40);
} catch (InterruptedException e) {
e.printStackTrace();
}
//要求 clerk 去增加产品
clerk.addProduct();
}
}
}
//消费者
class Consumer extends Thread{
private Clerk clerk;
public Consumer(Clerk clerk){
this.clerk = clerk;
}
@Override
public void run() {
System.out.println("=========消费者开始消费产品========");
while(true){
try {
Thread.sleep(90);
} catch (InterruptedException e) {
e.printStackTrace();
}
//要求 clerk 去减少产品
clerk.minusProduct();
}
}
}
//资源类
class Clerk {
private int productNum = 0;//产品数量
private static final int MAX_PRODUCT = 20;
private static final int MIN_PRODUCT = 1;
//增加产品
public synchronized void addProduct() {
if(productNum < MAX_PRODUCT){
productNum++;
System.out.println(Thread.currentThread().getName() +
"生产了第" + productNum + "个产品");
//唤醒消费者
this.notifyAll();
}else{
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//减少产品
public synchronized void minusProduct() {
if(productNum >= MIN_PRODUCT){
System.out.println(Thread.currentThread().getName() +
"消费了第" + productNum + "个产品");
productNum--;
//唤醒生产者
this.notifyAll();
}else{
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

 区分 sleep()和 wait()

相同点:一旦执行,都会使得当前线程结束执行状态,进入阻塞状态。

.不同点:

① 定义方法所属的类:sleep():Thread 中定义。 wait():Object 中定义

② 使用范围的不同:sleep()可以在任何需要使用的位置被调用; wait():必须使 用在同步代码块或同步方法中

③ 都在同步结构中使用的时候,是否释放同步监视器的操作不同:sleep():不会 释放同步监视器 ;wait():会释放同步监视器

④ 结束等待的方式不同:sleep():指定时间一到就结束阻塞。 wait():可以指定 时间也可以无限等待直到 notify 或 notifyAll。

 新增方式一:实现 Callable 接口

• 与使用 Runnable 相比, Callable 功能更强大些

– 相比 run()方法,可以有返回值

– 方法可以抛出异常

– 支持泛型的返回值(需要借助 FutureTask 类,获取返回结果)

• Future 接口(了解)

– 可以对具体 Runnable、Callable 任务的执行结果进行取消、查询是否完 成、获取结果等。 – FutureTask 是 Futrue 接口的唯一的实现类

– FutureTask 同时实现了 Runnable, Future 接口。它既可以作为 Runnable 被 线程执行,又可以作为 Future 得到 Callable 的返回值

• 缺点:在获取分线程执行结果的时候,当前线程(或是主线程)受阻塞,效率较低

代码举例:

class NumThread implements Callable {
 //2.实现 call 方法,将此线程需要执行的操作声明在 call()中
 @Override
 public Object call() throws Exception {
 int sum = 0;
 for (int i = 1; i <= 100; i++) {
 if (i % 2 == 0) {
 System.out.println(i);
 sum += i;
 }
 }
 return sum;
 }
}
public class CallableTest {
 public static void main(String[] args) {
 //3.创建 Callable 接口实现类的对象
 NumThread numThread = new NumThread();
 //4.将此 Callable 接口实现类的对象作为传递到 FutureTask 构造器中,
创建 FutureTask 的对象
 FutureTask futureTask = new FutureTask(numThread);
 //5.将 FutureTask 的对象作为参数传递到 Thread 类的构造器中,创建 Th
read 对象,并调用 start()
 new Thread(futureTask).start();
// 接收返回值
try {
 //6.获取 Callable 中 call 方法的返回值
 //get()返回值即为 FutureTask 构造器参数 Callable 实现类重写的 c
all()的返回值。
 Object sum = futureTask.get();
 System.out.println("总和为:" + sum);
 } catch (InterruptedException e) {
 e.printStackTrace();
 } catch (ExecutionException e) {
 e.printStackTrace();
 }
 }
}

新增方式二:使用线程池

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束 了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线 程需要时间。

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池 中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具

好处:

• 提高响应速度(减少了创建新线程的时间)

• 降低资源消耗(重复利用线程池中线程,不需要每次都创建)

• 便于线程管理

– corePoolSize:核心池的大小

– maximumPoolSize:最大线程数

– keepAliveTime:线程没有任务时最多保持多长时间后会终止

线程池相关 API

• JDK5.0 之前,我们必须手动自定义线程池。

从 JDK5.0 开始,Java 内置线程池相关的 API。

在 java.util.concurrent 包下提供了线程池相关 API:

ExecutorService 和 Executors。

• ExecutorService:真正的线程池接口。

常见子类 ThreadPoolExecutor

– void execute(Runnable command) :执行任务/命令,没有返回值, 一般用来执行 Runnable

– Future submit(Callable task):执行任务,有返回 值,一般又来执行 Callable

– void shutdown() :关闭连接池

• Executors:一个线程池的工厂类,通过此类的静态工厂方法可以创建多种类型的线 程池对象。

– Executors.newCachedThreadPool():创建一个可根据需要创建新线 程的线程池

– Executors.newFixedThreadPool(int nThreads); 创建一个可重用 固定线程数的线程池

– Executors.newSingleThreadExecutor() :创建一个只有一个线程的 线程池

– Executors.newScheduledThreadPool(int corePoolSize):创建 一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

举例 

class NumberThread implements Runnable{
 @Override
 public void run() {
 for(int i = 0;i <= 100;i++){
 if(i % 2 == 0){
 System.out.println(Thread.currentThread().getName() +
": " + i);
 }
 }
 }
}
class NumberThread1 implements Runnable{
 @Override
 public void run() {
 for(int i = 0;i <= 100;i++){
 if(i % 2 != 0){
 System.out.println(Thread.currentThread().getName() +
": " + i);
 }
 }
 }
}
class NumberThread2 implements Callable {
 @Override
 public Object call() throws Exception {
 int evenSum = 0;//记录偶数的和
 for(int i = 0;i <= 100;i++){
 if(i % 2 == 0){
 evenSum += i;
 }
 }
 return evenSum;
 }
}
public class ThreadPoolTest {
 public static void main(String[] args) {
 //1. 提供指定线程数量的线程池
 ExecutorService service = Executors.newFixedThreadPool(10);
 ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
// //设置线程池的属性
// System.out.println(service.getClass());//ThreadPoolExecutor
 service1.setMaximumPoolSize(50); //设置线程池中线程数的上限
 //2.执行指定的线程的操作。需要提供实现 Runnable 接口或 Callable 接
口实现类的对象
 service.execute(new NumberThread());//适合适用于 Runnable
 service.execute(new NumberThread1());//适合适用于 Runnable
 try {
 Future future = service.submit(new NumberThread2());//适合
使用于 Callable
 System.out.println("总和为:" + future.get());
 } catch (Exception e) {
 e.printStackTrace();
 }
 //3.关闭连接池
 service.shutdown();
 }
}

 String

String 的特性

• java.lang.String 类代表字符串。Java 程序中所有的字符串文字(例如 "hello" )都可以看作是实现此类的实例。

• 字符串是常量,用双引号引起来表示。它们的值在创建之后不能更改。

• 字符串 String 类型本身是 final 声明的,意味着我们不能继承 String。

• String 对象的字符内容是存储在一个字符数组 value[]中的。"abc" 等效于 char[] data={'h','e','l','l','o'}。

因为字符串对象设计为不可变,那么所以字符串有常量池来保存很多常量对 象。

String str1 = “abc”; 与 String str2 = new String(“abc”);的区别?

str2 首先指向堆中的一个字符串对象,然后堆中字符串的 value 数组指向常量 池中常量对象的 value 数组。

 • 字符串常量存储在字符串常量池,目的是共享

• 字符串非常量对象存储在堆中。

思考

String str2 = new String("hello"); 在内存中创建了几个对象?

答案:2个

一个堆对象   str2

一个常量池对象  “hello” 

intern() 

    public void test1(){
       String s1="a";
       String s="bcd";
        String s2=s1.intern();  //把拼接的结果放到常量池中
        String ss="a";
        String ss1=(s1+s).intern();
        System.out.println(ss1);
        String s3=new String("a");
        System.out.println(s2==ss);
        System.out.println(s2=="a");
        System.out.println(s2==s3);


    }

(1)常量+常量:结果是常量池。且常量池中不会存在相同内容的常 量。

(2)常量与变量 或 变量与变量:结果在堆中

(3)拼接后调用 intern 方法:返回值在常量池中 

concat 

public static void main(String[] args) {
String str = "hello";
String str2 = "world";
String str3 ="helloworld";
String str4 = "hello".concat("world");
String str5 = "hello"+"world";
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
}

concat 方法拼接,哪怕是两个常量对象拼接,结果也是在堆。

练习

public class string1 {
    String str = new String("good");
    char[] ch = { 't', 'e', 's', 't' };
    public void change(String str, char ch[]) {
        str = "test ok";
        ch[0] = 'b';
    }

    public static void main(String[] args) {
        string1 ex = new string1();
        ex.change(ex.str, ex.ch);
        System.out.print(ex.str + " and ");//
        System.out.println(ex.ch);
    }
}

       构造器

• public String() :初始化新创建的 String 对象,以使其表示空字符序列。

• String(String original): 初始化一个新创建的 String 对象,使其表示一个 与参数相同的字符序列;换句话说,新创建的字符串是该参数字符串的副本。

• public String(char[] value) :通过当前参数中的字符数组来构造新的 String。

• public String(char[] value,int offset, int count) :通过字符数组的 一部分来构造新的 String。

• public String(byte[] bytes) :通过使用平台的默认字符集解码当前参数中的 字节数组来构造新的 String。

• public String(byte[] bytes,String charsetName) :通过使用指定的字符 集解码当前参数中的字节数组来构造新的 String。

String 与其他结构间的转换

字符串 --> 基本数据类型、包装类:

• Integer 包装类的 public static int parseInt(String s):可以将由“数字”字符组成的字符串 转换为整型。

基本数据类型、包装类 --> 字符串:

• 调用 String 类的 public String valueOf(int n)可将 int 型转换为字符串

字符数组 --> 字符串:

• String 类的构造器:String(char[]) 和 String(char[],int offset,int length) 分别用字 符数组中的全部字符和部分字符创建字符串对象。

字符串 --> 字符数组:

• public char[] toCharArray():将字符串中的全部字符存放在一个字符数组中的方法。

• public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin):提供了将指定索 引范围内的字符串存放到数组中的方法。

字符串 --> 字节数组:(编码)

• public byte[] getBytes() :使用平台的默认字符集将此 String 编码为 byte 序列,并 将结果存储到一个新的 byte 数组中。

• public byte[] getBytes(String charsetName) :使用指定的字符集将此 String 编码到 byte 序列,并将结果存储到新的 byte 数组。

 字节数组 --> 字符串:(解码)

• String(byte[]):通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的 String。

• String(byte[],int offset,int length) :用指定的字节数组的一部分,即从数组起始位 置 offset 开始取 length 个字节构造一个字符串对象。

• String(byte[], String charsetName ) 或 new String(byte[], int, int,String charsetName ):解码,按照指定的编码方式进行解码。

 常用方法

boolean isEmpty():字符串是否为空

int length():返回字符串的长 度

String concat(xx):拼接

boolean equals(Object obj):比较字符 串是否相等,区分大小写

boolean equalsIgnoreCase(Object obj):比较字 符串是否相等,不区分大小写 

int compareTo(String other):比较字符串 大小,区分大小写,按照 Unicode 编码值比较大小

 String toLowerCase():将字符串中大写字母转为小写

String toUpperCase():将字符串中小写字母转为大写

String trim():去掉字符 串前后空白符

public String intern():结果在常量池中共享

boolean contains(xx):是否包含 xx

int indexOf(xx):从前往后找 当前字符串中 xx,即如果有返回第一次出现的下标,要是没有返回-1

int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出 现处的索引,从指定的索引开始.

int lastIndexOf(xx):从后往前找当前字 符串中 xx,即如果有返回最后一次出现的下标,要是没有返回-1

int lastIndexOf(String str, int fromIndex):返回指定子字符串在此字符串中最后一次 出现处的索引,从指定的索引开始反向搜索。

String substring(int beginIndex) :返回一个新的字符串,它是此字符串 的从 beginIndex 开始截取到最后的一个子字符串。

String substring(int beginIndex, int endIndex) :返回一个新字符串,它是此字符串从 beginIndex 开始截取到 endIndex(不包含)的一个子字符串。

char charAt(index):返回[index]位置的字符

char[] toCharArray(): 将此字符串转换为一个新的字符数组返回

static String valueOf(char[] data) :返回指定数组中表示该字符序列的 String 

static String copyValueOf(char[] data): 返回指定数组中 表示该字符序列的 String

static String copyValueOf(char[] data, int offset, int count):返回指定数组中表示该字符序列的 String

boolean startsWith(xx):测试此字符串是否以指定的前缀开始

boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的子 字符串是否以指定前缀开始

boolean endsWith(xx):测试此字符串是否 以指定的后缀结束

String replace(char oldChar, char newChar):返回一个新的字符串,它是 通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。 不支持正 则

String replace(CharSequence target, CharSequence replacement): 使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符 串。

String replaceAll(String regex, String replacement):使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。

String replaceFirst(String regex, String replacement):使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。

字符串相关类之可变字符序列:StringBuffer、StringBuilder 

因为 String 对象是不可变对象,虽然可以共享常量对象,但是对于频繁字符串 的修改和拼接操作,效率极低,空间消耗也比较高。因此,JDK 又在 java.lang 包提供了可变字符序列 StringBuffer 和 StringBuilder 类型。

常用 API 

(1)StringBuffer append(xx):提供了很多的 append()方法,用于进行字符串 追加的方式拼接 (2)StringBuffer delete(int start, int end):删除[start,end)之 间字符 (3)StringBuffer deleteCharAt(int index):删除[index]位置字符 (4) StringBuffer replace(int start, int end, String str):替换[start,end)范围的字符序列 为 str (5)void setCharAt(int index, char c):替换[index]位置字符 (6)char charAt(int index):查找指定 index 位置上的字符 (7)StringBuffer insert(int index, xx):在[index]位置插入 xx (8)int length():返回存储的字符数据的长 度 (9)StringBuffer reverse():反转

– StringBuffer:可变的字符序列;线程安全(方法有 synchronized 修饰),效 率低;底层使用 char[]数组存储 (JDK8.0 中)

– StringBuilder:可变的字符序列; jdk1.5 引入,线程不安全的,效率高;底 层使用 char[]数组存储(JDK8.0 中) 

日期时间 API 

Date

• 构造器:

– Date():使用无参构造器创建的对象可以获取本地当前时间。

– Date(long 毫秒数):把该毫秒值换算成日期时间对象

• 常用方法 – getTime(): 返回自 1970 年 1 月 1 日 00:00:00 GMT 以来此 Date 对象 表示的毫秒数。

– toString(): 把此 Date 对象转换为以下形式的 String: dow mon dd hh:mm:ss zzz yyyy 其中: dow 是一周中的某一天 (Sun, Mon, Tue, Wed, Thu, Fri, Sat),zzz 是时间标准。

    @Test
    public void test1(){
        Date d = new Date();
        System.out.println(d);  //输出当前时间 Sat Jul 06 15:32:30 CST 2024
    }
    @Test
    public void test2(){
        long time = System.currentTimeMillis();
        System.out.println(time);//1720251159856
        //当前系统时间距离 1970-1-1 0:0:0 0 毫秒的时间差,毫秒为单位
    }
    @Test
    public void test3(){
        Date d = new Date();
        long time = d.getTime();
        System.out.println(time);//1720251171347
    }
    @Test
    public void test4(){
        long time = 1720251171347L;
        Date d = new Date(time);
        System.out.println(d);//输出当前时间 Sat Jul 06 15:32:51 CST 2024
    }

 SimpleDateFormat

可以进行格式化:日期 --> 文本

可以进行解析:文本 --> 日期

• 构造器:

– SimpleDateFormat() :默认的模式和语言环境创建对象

– public SimpleDateFormat(String pattern):该构造方法可以用参数 pattern 指定的格式创建一个对象

• 格式化

– public String format(Date date):方法格式化时间对象 date

• 解析:

– public Date parse(String source):从给定字符串的开始解析文本,以生成 一个日期。

    //格式化
    @Test
    public void test1(){
        Date d = new Date();
        SimpleDateFormat sf = new SimpleDateFormat("yyyy 年 MM 月 dd 日 HH 时 mm 分 ss 秒 SSS 毫秒 ");
                //把 Date 日期转成字符串,按照指定的格式转
                String str = sf.format(d);
        System.out.println(str); //输出结果 2024 年 07 月 06 日 15 时 38 分 21 秒 044 毫秒
    }
    //解析
    @Test
    public void test2() throws ParseException {
        String str = "2024 年 07 月 06 日 15 时 38 分 21 秒 044 毫秒 ";
        SimpleDateFormat sf = new SimpleDateFormat("yyyy 年 MM 月 dd 日 HH 时 mm 分 ss 秒 SSS 毫秒 ");
                Date d = sf.parse(str);
        System.out.println(d);  //输出结果Sat Jul 06 15:38:21 CST 2024
    }

Calendar(日历)

.Date 类的 API 大部分被废弃了,替换为 Calendar。

• Calendar 类是一个抽象类,主用用于完成日期字段之间相互操作的功能。

• 获取 Calendar 实例的方法 – 使用 Calendar.getInstance()方法

• 一个 Calendar 的实例是系统时间的抽象表示,可以修改或获取 YEAR、MONTH、 DAYOFWEEK、HOUROFDAY 、MINUTE、SECOND 等 日历字段对应的时间值。

– public int get(int field):返回给定日历字段的值

– public void set(int field,int value) :将给定的日历字段设置为指定的值

– public void add(int field,int amount):根据日历的规则,为给定的日历字段 添加或者减去指定的时间量

– public final Date getTime():将 Calendar 转成 Date 对象

– public final void setTime(Date date):使用指定的 Date 对象重置 Calendar 的时间

 @Test
    public void test1(){
        Calendar c = Calendar.getInstance();
        System.out.println(c);
        int year = c.get(Calendar.YEAR);
        int month = c.get(Calendar.MONTH)+1;
        int day = c.get(Calendar.DATE);
        int hour = c.get(Calendar.HOUR_OF_DAY);
        int minute = c.get(Calendar.MINUTE);
        System.out.println(year + "-" + month + "-" + day + " " + hour + ":" + minute);//2024-7-6 15:44
    }

 新的日期时间 API

本地日期 (LocalDate)、本地时间(LocalTime)、本地日期时间(LocalDateTime)、 时区(ZonedDateTime)和持续时间(Duration)

now()/ now(ZoneId zone) 静态方法,根据当前时间创建对象/指定 时区的对象

    @Test
    public void test01(){
        LocalDate now = LocalDate.now();
        System.out.println(now);//2024-07-06
    }
    @Test
    public void test02(){
        LocalTime now = LocalTime.now();
        System.out.println(now);//15:50:01.635282900
    }

    @Test
    public void test03(){
        LocalDateTime now = LocalDateTime.now();
        System.out.println(now);//2024-07-06T15:50:27.159434700
    }
    @Test
    public void test04(){
        LocalDate lai = LocalDate.of(2019, 5, 13); //设置指定日期
        System.out.println(lai);//2019-05-13
    }
    @Test
    public void test05(){
        LocalDate lai = LocalDate.of(2024, 7, 6);
        System.out.println(lai.getDayOfYear());//今天是今年的第188天
    }
    @Test
    public void test06(){
        LocalDate lai = LocalDate.of(2024, 7, 6);
        LocalDate go = lai.plusDays(160);
        System.out.println(go);//2024年7月6号的100天后是2024年12月13日
    }
    @Test
    public void test7(){
        LocalDate now = LocalDate.now();
        LocalDate before = now.minusDays(100);
        System.out.println(before);//100天前是2024-03-28
    }

 Java 比较器

Java 实现对象排序的方式有两种:

– 自然排序:java.lang.Comparable

– 定制排序:java.util.Comparator

 自然排序:java.lang.Comparable

• Comparable 接口强行对实现它的每个类的对象进行整体排序。这种排序被称为类的 自然排序。

• 实现 Comparable 的类必须实现 compareTo(Object obj)方法,两个对象即通过 compareTo(Object obj) 方法的返回值来比较大小。如果当前对象 this 大于形参对象 obj,则返回正整数,如果当前对象 this 小于形参对象 obj,则返回负整数,如果当前 对象 this 等于形参对象 obj,则返回零

• Comparable 的典型实现:(默认都是从小到大排列的)

– String:按照字符串中字符的 Unicode 值进行比较

– Character:按照字符的 Unicode 值来进行比较

– 数值类型对应的包装类以及 BigInteger、BigDecimal:按照它们对应的数值 大小进行比较 – Boolean:true 对应的包装类实例大于 false 对应的包装类实例

– Date、Time 等:后面的日期时间比前面的日期时间大

• 实现 Comparable 接口的对象列表(和数组)可以通过 Collections.sort 或 Arrays.sort 进行自动排序。实现此接口的对象可以用作有序映射中的键或有序集合中 的元素,无需指定比较器。

• 对于类 C 的每一个 e1 和 e2 来说,当且仅当 e1.compareTo(e2) == 0 与 e1.equals(e2) 具有相同的 boolean 值时,类 C 的自然排序才叫做与 equals 一致。 建议(虽然不是必需的)最好使自然排序与 equals 一致。

举例

public class Student implements Comparable {
 private int id;
 private String name;
 private int score;
 private int age;
 public Student(int id, String name, int score, int age) {
 this.id = id;
 this.name = name;
 this.score = score;
 this.age = age;
 }
//set,get省略
@Override
 public String toString() {
 return "Student{" +
 "id=" + id +
 ", name='" + name + '\'' +
 ", score=" + score +
 ", age=" + age +
 '}';
 }
 @Override
 public int compareTo(Object o) {
 //这些需要强制,将 o 对象向下转型为 Student 类型的变量,才能调用 Student 类中的属性
 //默认按照学号比较大小
 Student stu = (Student) o;
 return this.id - stu.id;
 }
}

测试

public class TestStudent {
 public static void main(String[] args) {
 Student[] arr = new Student[5];
 arr[0] = new Student(3,"张三",90,23);
 arr[1] = new Student(1,"熊大",100,22);
 arr[2] = new Student(5,"王五",75,25);
 arr[3] = new Student(4,"李四",85,24);
 arr[4] = new Student(2,"熊二",85,18);
 //单独比较两个对象
 System.out.println(arr[0].compareTo(arr[1]));
 System.out.println(arr[1].compareTo(arr[2]));
 System.out.println(arr[2].compareTo(arr[2]));
 System.out.println("所有学生:");
 for (int i = 0; i < arr.length; i++) {
 System.out.println(arr[i]);
 }
 System.out.println("按照学号排序:");
 for (int i = 1; i < arr.length; i++) {
 for (int j = 0; j < arr.length-i; j++) {
 if(arr[j].compareTo(arr[j+1])>0){
 Student temp = arr[j];
 arr[j] = arr[j+1];
 arr[j+1] = temp;
 }
 }
 }
 for (int i = 0; i < arr.length; i++) {
 System.out.println(arr[i]);
 }
 }
}

举例二:

package date;

import org.junit.Test;

import java.util.Arrays;

public class Goods implements Comparable{
    private String name;
    private double price;
    //按照价格,比较商品的大小


    public Goods(String name, double price) {
        this.name = name;
        this.price = price;
    }

    @Override
    public String toString() {
        return "Goods{" +
                "name='" + name + '\'' +
                ", price=" + price +
                '}';
    }

    public String getName() {
        return name;
    }

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

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    @Override
    public int compareTo(Object o) {
        if(o instanceof Goods) {
            Goods other = (Goods) o;
            if (this.price > other.price) {
                return 1;
            } else if (this.price < other.price) {
                return -1;
            }
            return 0;
        }
        throw new RuntimeException("输入的数据类型不一致");
    }
    //构造器、getter、setter、toString()方法略

    public static void main(String[] args) {


        Goods[] all = new Goods[4];
        all[0] = new Goods("《红楼梦》", 100);
        all[1] = new Goods("《西游记》", 80);
        all[2] = new Goods("《三国演义》", 140);
        all[3] = new Goods("《水浒传》", 120);
        Arrays.sort(all);
        System.out.println(Arrays.toString(all));
    }
}

这段Java代码定义了一个名为`Goods`的类,该类实现了`Comparable`接口,并重写了`compareTo`方法。`Goods`类有两个属性:`name`和`price`,分别表示商品的名称和价格。

`Goods`类有一个构造函数,用于初始化商品的名称和价格。同时,它还提供了一个`toString`方法,用于返回商品的详细信息。

`Goods`类还提供了`getName`、`setName`、`getPrice`和`setPrice`方法,用于获取和设置商品的名称和价格。

在`main`方法中,我们创建了一个`Goods`数组,并为其赋值。然后,我们使用`Arrays.sort`方法对数组进行排序。`Arrays.sort`方法会根据`Goods`类中重写的`compareTo`方法对数组中的元素进行排序。

最后,我们使用`System.out.println`方法打印排序后的`Goods`数组。

 定制排序:java.util.Comparator

思考

– 当元素的类型没有实现 java.lang.Comparable 接口而又不方便修改代码 (例如:一些第三方的类,你只有.class 文件,没有源文件)

– 如果一个类,实现了 Comparable 接口,也指定了两个对象的比较大小的 规则,但是此时此刻我不想按照它预定义的方法比较大小,但是我又不能 随意修改,因为会影响其他地方的使用,怎么办?

JDK 在设计类库之初,也考虑到这种情况,所以又增加了一个 java.util.Comparator 接 口。强行对多个对象进行整体排序的比较。

– 重写 compare(Object o1,Object o2)方法,比较 o1 和 o2 的大小:如果方法 返回正整数,则表示 o1 大于 o2;如果返回 0,表示相等;返回负整数, 表示 o1 小于 o2。

– 可以将 Comparator 传递给 sort 方法(如 Collections.sort 或 Arrays.sort),从而允许在排序顺序上实现精确控制

举例

//定义定制比较器类
public class StudentScoreComparator implements Comparator {
 @Override
 public int compare(Object o1, Object o2) {
 Student s1 = (Student) o1;
 Student s2 = (Student) o2;
 int result = s1.getScore() - s2.getScore();
 return result != 0 ? result : s1.getId() - s2.getId();
 }
public static void main(String[] args) {
 Student[] arr = new Student[5];
 arr[0] = new Student(3, "张三", 90, 23);
 arr[1] = new Student(1, "熊大", 100, 22);
 arr[2] = new Student(5, "王五", 75, 25);
 arr[3] = new Student(4, "李四", 85, 24);
 arr[4] = new Student(2, "熊二", 85, 18);
 System.out.println("所有学生:");
 for (int i = 0; i < arr.length; i++) {
 System.out.println(arr[i]);
 }
 System.out.println("按照成绩排序");
 StudentScoreComparator sc = new StudentScoreComparator();
 for (int i = 1; i < arr.length; i++) {
 for (int j = 0; j < arr.length - i; j++) {
 if (sc.compare(arr[j], arr[j + 1]) > 0) {
 Student temp = arr[j];
 arr[j] = arr[j + 1];
 arr[j + 1] = temp;
 }
 }
 }
 for (int i = 0; i < arr.length; i++) {
 System.out.println(arr[i]);
 }
 }
}

//或者
@Test
public void test01() {
 Student[] students = new Student[5];
 students[0] = new Student(3, "张三", 90, 23);
 students[1] = new Student(1, "熊大", 100, 22);
 students[2] = new Student(5, "王五", 75, 25);
 students[3] = new Student(4, "李四", 85, 24);
 students[4] = new Student(2, "熊二", 85, 18);
 System.out.println(Arrays.toString(students));
 //定制排序
 StudentScoreComparator sc = new StudentScoreComparator();
 Arrays.sort(students, sc);
 System.out.println("排序之后:");
 System.out.println(Arrays.toString(students));
}

 再举例

Goods[] all = new Goods[4];
all[0] = new Goods("War and Peace", 100);
all[1] = new Goods("Childhood", 80);
all[2] = new Goods("Scarlet and Black", 140);
all[3] = new Goods("Notre Dame de Paris", 120);
Arrays.sort(all, new Comparator() {
 @Override
 public int compare(Object o1, Object o2) {
 Goods g1 = (Goods) o1;
 Goods g2 = (Goods) o2;
 return g1.getName().compareTo(g2.getName());
 }
});
System.out.println(Arrays.toString(all));

系统相关类

java.lang.System 类

• System 类代表系统,系统级的很多属性和控制方法都放置在该类的内部。该类位于 java.lang 包。

• 由于该类的构造器是 private 的,所以无法创建该类的对象。其内部的成员变量和成 员方法都是 static 的,所以也可以很方便的进行调用。

• 成员变量 Scanner scan = new Scanner(System.in);

– System 类内部包含 in、out 和 err 三个成员变量,分别代表标准输入流 (键盘输入),标准输出流(显示器)和标准错误输出流(显示器)。

成员方法

– native long currentTimeMillis(): 该方法的作用是返回当前的计 算机时间,时间的表达格式为当前计算机时间和 GMT 时间(格林威治时 间)1970 年 1 月 1 号 0 时 0 分 0 秒所差的毫秒数。

– void exit(int status): 该方法的作用是退出程序。其中 status 的值 为 0 代表正常退出,非零代表异常退出。使用该方法可以在图形界面编程 中实现程序的退出功能等。

– void gc(): 该方法的作用是请求系统进行垃圾回收。至于系统是否立刻 回收,则取决于系统中垃圾回收算法的实现以及系统执行时的情况。 

– String getProperty(String key): 该方法的作用是获得系统中属 性名为 key 的属性对应的值。系统中常见的属性名以及属性的作用如下表 所示:

  @Test
    public void test02(){
        String javaVersion = System.getProperty("java.version");
        System.out.println("java 的 version:" + javaVersion);
        String javaHome = System.getProperty("java.home");
        System.out.println("java 的 home:" + javaHome);
        String osName = System.getProperty("os.name");
        System.out.println("os 的 name:" + osName);
        String osVersion = System.getProperty("os.version");
        System.out.println("os 的 version:" + osVersion);
        String userName = System.getProperty("user.name");
        System.out.println("user 的 name:" + userName);
        String userHome = System.getProperty("user.home");
        System.out.println("user 的 home:" + userHome);
        String userDir = System.getProperty("user.dir");
        System.out.println("user 的 dir:" + userDir);
        long time = System.currentTimeMillis();
        System.out.println("现在的系统时间距离 1970 年 1 月 1 日凌晨:" + time + "毫秒");
        System.exit(0);
        System.out.println("over");//不会执行
    }

java.lang.Runtime 类

每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环 境相连接。 public static Runtime getRuntime(): 返回与当前 Java 应用程序相关的运 行时对象。应用程序不能创建自己的 Runtime 类实例。

public long totalMemory():返回 Java 虚拟机中初始化时的内存总量。此方 法返回的值可能随时间的推移而变化,这取决于主机环境。默认为物理电脑内 存的 1/64。

public long maxMemory():返回 Java 虚拟机中最大程度能使用的内存总量。 默认为物理电脑内存的 1/4。

public long freeMemory():回 Java 虚拟机中的空闲内存量。调用 gc 方法 可能导致 freeMemory 返回值的增加。

    public static void main(String[] args) {
        Runtime runtime = Runtime.getRuntime();
        long initialMemory = runtime.totalMemory(); //获取虚拟机初始化时堆内存总量
        long maxMemory = runtime.maxMemory(); //获取虚拟机最大堆内存总量
        String str = "";
        //模拟占用内存
        for (int i = 0; i < 10000; i++) {
            str += i;
        }
        long freeMemory = runtime.freeMemory(); //获取空闲堆内存总量
        System.out.println("总内存:" + initialMemory / 1024 / 1024 *
                64 + "MB");
        System.out.println("总内存:" + maxMemory / 1024 / 1024 * 4 +
                "MB");
        System.out.println("空闲内存:" + freeMemory / 1024 / 1024 +
                "MB") ;
        System.out.println("已用内存:" + (initialMemory-freeMemory) /
                1024 / 1024 + "MB");
    }
    

 

java.lang.Math

• public static double abs(double a) :返回 double 值的绝对值。

• public static double ceil(double a) :返回大于等于参数的最小的整数。

• public static double floor(double a) :返回小于等于参数最大的整数。

• public static long round(double a) :返回最接近参数的 long。(相当于四 舍五入方法)

• public static double pow(double a,double b):返回 a 的 b 幂次方法

• public static double sqrt(double a):返回 a 的平方根

• public static double random():返回[0,1)的随机值

• public static final double PI:返回圆周率

• public static double max(double x, double y):返回 x,y 中的最大值

• public static double min(double x, double y):返回 x,y 中的最小值

• 其它:acos,asin,atan,cos,sin,tan 三角函数

 java.math 包

BigInteger

java.math 包的 BigInteger 可以表示不可变的任意精度的整数。BigInteger 提供所有 Java 的基本整数操作符的对应物,并提供 java.lang.Math 的所有相关方法。另外, BigInteger 还提供以下运算:模算术、GCD 计算、质数测试、素数生成、位操作以 及一些其他操作。

@Test
public void test01(){
 //long bigNum = 123456789123456789123456789L;
 BigInteger b1 = new BigInteger("12345678912345678912345678");
 BigInteger b2 = new BigInteger("78923456789123456789123456789");
 //System.out.println("和:" + (b1+b2));//错误的,无法直接使用+进行
求和
 System.out.println("和:" + b1.add(b2));
 System.out.println("减:" + b1.subtract(b2));
 System.out.println("乘:" + b1.multiply(b2));
 System.out.println("除:" + b2.divide(b1));
 System.out.println("余:" + b2.remainder(b1));
}

BigDecimal 

@Test
public void test03(){
 BigInteger bi = new BigInteger("12433241123");
 BigDecimal bd = new BigDecimal("12435.351");
 BigDecimal bd2 = new BigDecimal("11");
 System.out.println(bi);
 // System.out.println(bd.divide(bd2));
 System.out.println(bd.divide(bd2, BigDecimal.ROUND_HALF_UP));
 System.out.println(bd.divide(bd2, 15, BigDecimal.ROUND_HALF_UP));
}

 java.util.Random

用于产生随机数

@Test
public void test04(){
 Random r = new Random();
 System.out.println("随机整数:" + r.nextInt());
 System.out.println("随机小数:" + r.nextDouble());
 System.out.println("随机布尔值:" + r.nextBoolean());
}

集合框架

Java 集合可分为 Collection 和 Map 两大体系:

• Collection 接口:用于存储一个一个的数据,也称单列数据集合。

– List 子接口:用来存储有序的、可以重复的数据(主要用来替换数组,"动 态"数组)

• 实现类:ArrayList(主要实现类)、LinkedList、Vector

• Set 子接口:用来存储无序的、不可重复的数据(类似于高中讲的"集合")

– 实现类:HashSet(主要实现类)、LinkedHashSet、TreeSet

• Map 接口:用于存储具有映射关系“key-value 对”的集合,即一对一对的数据,也称 双列数据集合。(类似于高中的函数、映射。(x1,y1),(x2,y2) ---> y = f(x) )

– HashMap(主要实现类)、LinkedHashMap、TreeMap、Hashtable、 Properties

 常用方法

(1)add(E obj):添加元素对象到当前集合中

(2)addAll(Collection other): 添加 other 集合中的所有元素对象到当前集合中

(3)int size():获取当前集合中实际存储的元素个数

(4)boolean isEmpty():判断当前集合是否为空集合

(5)boolean contains(Object obj):判 断当前集合中是否存在一个与 obj 对象 equals 返回 true 的元素

(6)boolean containsAll(Collection coll):判断 coll 集合中的元素是否在当前集合中都存在。 即 coll 集合是否是当前集合的“子集” (

7)boolean equals(Object obj):判断当 前集合与 obj 是否相等

(8)void clear():清空集合元素

(9) boolean remove(Object obj) :从当前 集合中删除第一个找到的与 obj 对象 equals 返回 true 的元素。

(10)boolean removeAll(Collection coll):从当前集合中删除所有与 coll 集合中相同的元素。 即 this = this - this ∩ coll

(11)boolean retainAll(Collection coll):从当前集合 中删除两个集合中不同的元素,使得当前集合仅保留与 coll 集合中的元素相同 的元素,即当前集合中仅保留两个集合的交集,即 this = this ∩ coll;

(12)Object[] toArray():返回包含当前集合中所有元素的数组

(13) hashCode():获取集合对象的哈希值

(14)iterator():返回迭代器对象,用于 集合遍历

 Iterator 接口

Iterator 接口也是 Java 集合中的一员,但它与 Collection、Map 接口有所不同。

– Collection 接口与 Map 接口主要用于存储元素

– Iterator,被称为迭代器接口,本身并不提供存储对象的能力,主要用于 遍历 Collection 中的元素

• Collection 接口继承了 java.lang.Iterable 接口,该接口有一个 iterator()方法,那么所有 实现了 Collection 接口的集合类都有一个 iterator()方法,用以返回一个实现了 Iterator 接口的对象。

– public Iterator iterator(): 获取集合对应的迭代器,用来遍历集合 中的元素的。

– 集合对象每次调用 iterator()方法都得到一个全新的迭代器对象,默认游标 都在集合的第一个元素之前。

• Iterator 接口的常用方法如下:

– public E next():返回迭代的下一个元素。

– public boolean hasNext():如果仍有元素可以迭代,则返回 true。

• 注意:在调用 it.next()方法之前必须要调用 it.hasNext()进行检测。若不调用,且下一 条记录无效,直接调用 it.next()会抛出 NoSuchElementException 异常。

@Test
 public void test01(){
 Collection coll = new ArrayList();
 coll.add("小李广");
 coll.add("扫地僧");
 coll.add("石破天");
 Iterator iterator = coll.iterator();
 System.out.println(iterator.next());
 System.out.println(iterator.next());
 System.out.println(iterator.next());
 System.out.println(iterator.next()); //报 NoSuchElementExcepti
on 异常
 }
@Test
 public void test02(){
 Collection coll = new ArrayList();
 coll.add("小李广");
 coll.add("扫地僧");
 coll.add("石破天");
 Iterator iterator = coll.iterator();//获取迭代器对象
 while(iterator.hasNext()) {//判断是否还有元素可迭代
 System.out.println(iterator.next());//取出下一个元素
 }
 }
}

 迭代器的执行原理

Iterator 迭代器对象在遍历集合时,内部采用指针的方式来跟踪集合中的元素

        使用 Iterator 迭代器删除元素:java.util.Iterator 迭代器中有一个方法:void remove() ;

Iterator iter = coll.iterator();//回到起点
while(iter.hasNext()){
 Object obj = iter.next();
 if(obj.equals("Tom")){
 iter.remove();
 }
}

• Iterator 可以删除集合的元素,但是遍历过程中通过迭代器对象的 remove 方法,不是 集合对象的 remove 方法。

 • 如果还未调用 next()或在上一次调用 next() 方法之后已经调用了 remove() 方法,再 调用 remove()都会报 IllegalStateException。

• Collection 已经有 remove(xx)方法了,为什么 Iterator 迭代器还要提供删除方法呢?因 为迭代器的 remove()可以按指定的条件进行删除。


@Test
 public void test01(){
 Collection coll = new ArrayList();
 coll.add(1);
 coll.add(2);
 coll.add(3);
 coll.add(4);
 coll.add(5);
 coll.add(6);
 Iterator iterator = coll.iterator();
 while(iterator.hasNext()){
 Integer element = (Integer) iterator.next();
 if(element % 2 == 0){
 iterator.remove();
 }
 }
 System.out.println(coll);
 }

 在 JDK8.0 时,Collection 接口有了 removeIf 方法,即可以根据条件删除。

@Test
 public void test01(){
 Collection coll = new ArrayList();
 coll.add("小李广");
 coll.add("扫地僧");
 coll.add("石破天");
 coll.add("佛地魔");
 System.out.println("coll = " + coll);
 coll.removeIf(new Predicate() {
 @Override
 public boolean test(Object o) {
 String str = (String) o;
 return str.contains("地");
 }
 });
 System.out.println("删除包含\"地\"字的元素之后 coll = " + coll);
 }

foreach 循环

foreach 循环(也称增强 for 循环)是 JDK5.0 中定义的一个高级 for 循环,专门用来 遍历数组和集合的。

foreach 循环的语法格式

for(元素的数据类型 局部变量 : Collection 集合或数组){ //操作局部变量的输出操作 }

举例:

 @Test
 public void test01(){
 Collection coll = new ArrayList();
 coll.add("小李广");
 coll.add("扫地僧");
 coll.add("石破天");
//foreach 循环其实就是使用 Iterator 迭代器来完成元素的遍历的。
 for (Object o : coll) {
 System.out.println(o);
 }
 }

List

List 集合类中元素有序、且可重复,集合中的每个元素都有其对应的顺序索引。

JDK API 中 List 接口的实现类常用的有:ArrayList、LinkedList 和 Vector。

插入元素

void add(int index, Object ele):在 index 位置插入 ele 元素

boolean addAll(int index, Collection eles):从 index 位置开始将 eles 中的所有 元素添加进来

获取元素

Object get(int index):获取指定 index 位置的元素

List subList(int fromIndex, int toIndex):返回从 fromIndex 到 toIndex 位置的 子集合

获取元素索引

int indexOf(Object obj):返回 obj 在集合中首次出现的位置

int lastIndexOf(Object obj):返回 obj 在当前集合中末次出现的位置

删除和替换元素

Object remove(int index):移除指定 index 位置的元素,并返回此元 素

Object set(int index, Object ele):设置指定 index 位置的元素为 ele

List 接口主要实现类:ArrayList 

ArrayList 是 List 接口的主要实现类

本质上,ArrayList 是对象引用的一个”变长”数组

 Arrays.asList(…) 方法返回的 List 集合,既不是 ArrayList 实例,也不是 Vector 实 例。 Arrays.asList(…) 返回值是一个固定长度的 List 集

List 的实现类之二:LinkedList (了解)

对于频繁的插入或删除元素的操作,建议使用 LinkedList 类,效率较高。这是由底层 采用链表(双向链表)结构存储数据决定的。

特有方法:

.– void addFirst(Object obj)

– void addLast(Object obj)

.– Object getFirst()

– Object getLast()

– Object removeFirst()

– Object removeLast()

List 的实现类之三:Vector (了解)

• Vector 是一个古老的集合,JDK1.0 就有了。大多数操作与 ArrayList 相同,区别之处 在于 Vector 是线程安全的。

• 在各种 List 中,最好把 ArrayList 作为默认选择。当插入、删除频繁时,使用 LinkedList;Vector 总是比 ArrayList 慢,所以尽量避免使用。

• 特有方法:

– void addElement(Object obj)

– void insertElementAt(Object obj,int index)

– void setElementAt(Object obj,int index)

– void removeElement(Object obj)

– void removeAllElements()

Collection 子接口 2:Set 

Set 接口概述

• Set 接口是 Collection 的子接口,Set 接口相较于 Collection 接口没有提供额外的方法

• Set 集合不允许包含相同的元素,如果试把两个相同的元素加入同一个 Set 集合中, 则添加操作失败。

• Set 集合支持的遍历方式和 Collection 集合一样:foreach 和 Iterator。

• Set 的常用实现类有:HashSet、TreeSet、LinkedHashSet。

Set 主要实现类:HashSet 

• HashSet 是 Set 接口的主要实现类,大多数时候使用 Set 集合时都使用这个实现 类。

• HashSet 按 Hash 算法来存储集合中的元素,因此具有很好的存储、查找、删除性 能。

• HashSet 具有以下特点:

– 不能保证元素的排列顺序

– HashSet 不是线程安全的

– 集合元素可以是 null

• HashSet 集合判断两个元素相等的标准:两个对象通过 hashCode() 方法得到的 哈希值相等,并且两个对象的 equals()方法返回值为 true。

• 对于存放在 Set 容器中的对象,对应的类一定要重写 hashCode()和 equals(Object obj)方法,以实现对象相等规则。即:“相等的对象必须具有相等的散列码”。

• HashSet 集合中元素的无序性,不等同于随机性。这里的无序性与元素的添加位置有 关。具体来说:我们在添加每一个元素到数组中时,具体的存储位置是由元素的 hashCode()调用后返回的 hash 值决定的。导致在数组中每个元素不是依次紧密存放 的,表现出一定的无序性。

HashSet 中添加元素的过程

第 1 步:当向 HashSet 集合中存入一个元素时,HashSet 会调用该对象的 hashCode() 方法得到该对象的 hashCode 值,然后根据 hashCode 值,通过某个散 列函数决定该对象在 HashSet 底层数组中的存储位置。

第 2 步:如果要在数组中存储的位置上没有元素,则直接添加成功。

第 3 步:如果要在数组中存储的位置上有元素,则继续比较:

– 如果两个元素的 hashCode 值不相等,则添加成功;

– 如果两个元素的 hashCode()值相等,则会继续调用 equals()方法:

• 如果 equals()方法结果为 false,则添加成功。

• 如果 equals()方法结果为 true,则添加失败

第 2 步添加成功,元素会保存在底层数组中。

第 3 步两种添加成功的操作,由于该底层数组的位置已经有元素 了,则会通过链表的方式继续链接,存储。

举例

public class TestHashSet {
 @Test
 public void test01(){
 HashSet set = new HashSet();
 set.add("张三");
 set.add("张三");
 set.add("李四");
 set.add("王五");
 set.add("王五");
 set.add("赵六");
 System.out.println("set = " + set);//不允许重复,无序
 }
 @Test
 public void test02(){
 HashSet set = new HashSet();
 set.add(new MyDate(2021,1,1));
 set.add(new MyDate(2021,1,1));
 set.add(new MyDate(2022,2,4));
 set.add(new MyDate(2022,2,4));
 System.out.println("set = " + set);//不允许重复,无序
 }
}
public class MyDate {
 private int year;
 private int month;
 private int day;
 public MyDate(int year, int month, int day) {
 this.year = year;
 this.month = month;
 this.day = day;
 }
 @Override
 public boolean equals(Object o) {
 if (this == o) return true;
 if (o == null || getClass() != o.getClass()) return false;
 MyDate myDate = (MyDate) o;
 return year == myDate.year &&
 month == myDate.month &&
 day == myDate.day;
 }
 @Override
 public int hashCode() {
 return Objects.hash(year, month, day);
 }
 @Override
 public String toString() {
 return "MyDate{" +
 "year=" + year +
 ", month=" + month +
 ", day=" + day +
 '}';
}}

重写 hashCode() 方法的基本原则

• 在程序运行时,同一个对象多次调用 hashCode() 方法应该返回相同的值。

• 当两个对象的 equals() 方法比较返回 true 时,这两个对象的 hashCode() 方法的返 回值也应相等。

• 对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值。 注意:如果两个元素的 equals() 方法返回 true,但它们的 hashCode() 返回值不相等,hashSet 将会把它们存储在不同的位置, 但依然可以添加成功。

重写 equals()方法的基本原则 

• 重写 equals 方法的时候一般都需要同时复写 hashCode 方法。通常参与计算 hashCode 的对象的属性也应该参与到 equals()中进行计算。

• 推荐:开发中直接调用 Eclipse/IDEA 里的快捷键自动重写 equals()和 hashCode()方法 即可。

Set 实现类之二:LinkedHashSet (了解)

• LinkedHashSet 是 HashSet 的子类,不允许集合元素重复。

• LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,但它同时使用双向 链表维护元素的次序,这使得元素看起来是以添加顺序保存的。

• LinkedHashSet 插入性能略低于 HashSet,但在迭代访问 Set 里的全部元素时有很 好的性能

Set 实现类之三:TreeSet  (了解)

• TreeSet 是 SortedSet 接口的实现类,TreeSet 可以按照添加的元素的指定的属性的 大小顺序进行遍历。

• TreeSet 底层使用红黑树结构存储数据

• TreeSet 特点:不允许重复、实现排序(自然排序或定制排序)

• TreeSet 两种排序方法:自然排序和定制排序。默认情况下,TreeSet 采用自然排 序。

– 自然排序:TreeSet 会调用集合元素的 compareTo(Object obj) 方法来比 较元素之间的大小关系,然后将集合元素按升序(默认情况)排列。

• 如果试图把一个对象添加到 TreeSet 时,则该对象的类必须实现 Comparable 接口。

• 实现 Comparable 的类必须实现 compareTo(Object obj) 方法,两 个对象即通过 compareTo(Object obj) 方法的返回值来比较大小

– 定制排序:如果元素所属的类没有实现 Comparable 接口,或不希望按照 升序(默认情况)的方式排列元素或希望按照其它属性大小进行排序,则考虑 使用定制排序。定制排序,通过 Comparator 接口来实现。需要重写 compare(T o1,T o2)方法。

• 利用 int compare(T o1,T o2)方法,比较 o1 和 o2 的大小:如果方法 返回正整数,则表示 o1 大于 o2;如果返回 0,表示相等;返回负 整数,表示 o1 小于 o2。

• 要实现定制排序,需要将实现 Comparator 接口的实例作为形参传 递给 TreeSet 的构造器。

• 因为只有相同类的两个实例才会比较大小,所以向 TreeSet 中添加的应该是同一个 类的对象。

• 对于 TreeSet 集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过 compareTo(Object obj) 或 compare(Object o1,Object o2)方法比较返回 值。返回值为 0,则认为两个对象相等。

Map 接口 

现实生活与开发中,我们常会看到这样的一类集合:用户 ID 与账户信息、学生 姓名与考试成绩、IP 地址与主机名等,这种一一对应的关系,就称作映射。

• Map 与 Collection 并列存在。用于保存具有映射关系的数据:key-value – Collection 集合称为单列集合,元素是孤立存在的(理解为单身)。 – Map 集合称为双列集合,元素是成对存在的(理解为夫妻)。

• Map 中的 key 和 value 都可以是任何引用类型的数据。但常用 String 类作为 Map 的“键”。

• Map 接口的常用实现类:HashMap、LinkedHashMap、TreeMap 和 `Properties。其中,HashMap 是 Map 接口使用频率最高的实现类。

• Map 中的 key 用 Set 来存放,不允许重复,即同一个 Map 对象所对应的类,须 重写 hashCode()和 equals()方法

• key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到唯一的、确定 的 value,不同 key 对应的 value 可以重复。value 所在的类要重写 equals()方法。

• key 和 value 构成一个 entry。所有的 entry 彼此之间是无序的、不可重复的。

Map 接口的常用方法

添加、修改操作:

– Object put(Object key,Object value):将指定 key-value 添加到(或修改)当前 map 对象中

– void putAll(Map m):将 m 中的所有 key-value 对存放到当前 map 中 

删除操作:

– Object remove(Object key):移除指定 key 的 key-value 对,并返回 value

– void clear():清空当前 map 中的所有数据

元素查询的操作:

– Object get(Object key):获取指定 key 对应的 value

– boolean containsKey(Object key):是否包含指定的 key

– boolean containsValue(Object value):是否包含指定的 value

– int size():返回 map 中 key-value 对的个数

– boolean isEmpty():判断当前 map 是否为空

– boolean equals(Object obj):判断当前 map 和参数对象 obj 是否相等

元视图操作的方法:

– Set keySet():返回所有 key 构成的 Set 集合

– Collection values():返回所有 value 构成的 Collection 集合

– Set entrySet():返回所有 key-value 对构成的 Set 集合

public static void main(String[] args) {
 //创建 map 对象
 HashMap map = new HashMap();
 //添加元素到集合
 map.put("黄晓明", "杨颖");
 map.put("李晨", "李小璐");
 map.put("李晨", "范冰冰");
 map.put("邓超", "孙俪");
 System.out.println(map);
 //删除指定的 key-value
 System.out.println(map.remove("黄晓明"));
 System.out.println(map);
 //查询指定 key 对应的 value
 System.out.println(map.get("邓超"));
 System.out.println(map.get("黄晓明"));
 }
@Test
public void test(){

HashMap map = new HashMap();
 map.put("许仙", "白娘子");
 map.put("董永", "七仙女");
 map.put("牛郎", "织女");
 map.put("许仙", "小青");
 System.out.println("所有的 key:");
 Set keySet = map.keySet();
 for (Object key : keySet) {
 System.out.println(key);
 }
 System.out.println("所有的 value:");
 Collection values = map.values();
 for (Object value : values) {
 System.out.println(value);
 }
 System.out.println("所有的映射关系:");
 Set entrySet = map.entrySet();
 for (Object mapping : entrySet) {
 //System.out.println(entry);
 Map.Entry entry = (Map.Entry) mapping;
 System.out.println(entry.getKey() + "->" + entry.getValue());


}

 Map 的主要实现类:HashMap

.HashMap 是 Map 接口使用频率最高的实现类

HashMap 是线程不安全的。允许添加 null 键和 null 值。

• 存储数据采用的哈希表结构,底层使用一维数组+单向链表+红黑树进行 key-value 数据的存储。与 HashSet 一样,元素的存取顺序不能保证一致。

• HashMap 判断两个 key 相等的标准是:两个 key 的 hashCode 值相等,通过 equals() 方法返回 true。

• HashMap 判断两个 value 相等的标准是:两个 value 通过 equals() 方法返回 true。

练习

添加你喜欢的歌手以及你喜欢他唱过的歌曲

方式一:

public class SingerTest1 {
 public static void main(String[] args) {
 //创建一个 HashMap 用于保存歌手和其歌曲集
 HashMap singers = new HashMap();
 //声明一组 key,value
 String singer1 = "周杰伦";
 ArrayList songs1 = new ArrayList();
 songs1.add("双节棍");
 songs1.add("本草纲目");
 songs1.add("夜曲");
 songs1.add("稻香");
 //添加到 map 中
 singers.put(singer1,songs1);
 //声明一组 key,value
 String singer2 = "陈奕迅";
 List songs2 = Arrays.asList("浮夸", "十年", "红玫瑰", "好久不见
", "孤勇者");
 //添加到 map 中
 singers.put(singer2,songs2);
 //遍历 map
 Set entrySet = singers.entrySet();
 for(Object obj : entrySet){
 Map.Entry entry = (Map.Entry)obj;
 String singer = (String) entry.getKey();
 List songs = (List) entry.getValue();
 System.out.println("歌手:" + singer);
 System.out.println("歌曲有:" + songs);
 }
 }
}

方式二:

//方式 2:改为 HashSet 实现
public class SingerTest2 {
@Test
public void test1() {
Singer singer1 = new Singer("周杰伦");
Singer singer2 = new Singer("陈奕迅");
Song song1 = new Song("双节棍");
Song song2 = new Song("本草纲目");
Song song3 = new Song("夜曲");
Song song4 = new Song("浮夸");
Song song5 = new Song("十年");
Song song6 = new Song("孤勇者");
HashSet h1 = new HashSet();// 放歌手一的歌曲
h1.add(song1);
h1.add(song2);
h1.add(song3);
HashSet h2 = new HashSet();// 放歌手二的歌曲
h2.add(song4);
h2.add(song5);
h2.add(song6);
HashMap hashMap = new HashMap();// 放歌手和他对应的歌曲
hashMap.put(singer1, h1);
hashMap.put(singer2, h2);
for (Object obj : hashMap.keySet()) {
System.out.println(obj + "=" + hashMap.get(obj));
}
}
}//歌曲
public class Song implements Comparable{
private String songName;//歌名
public Song() {
super();
}
public Song(String songName) {
super();
this.songName = songName;
}
public String getSongName() {
return songName;
}
public void setSongName(String songName) {
this.songName = songName;
}
@Override
public String toString() {
return "《" + songName + "》";
}
@Override
public int compareTo(Object o) {
if(o == this){
return 0;
}
if(o instanceof Song){
Song song = (Song)o;
return songName.compareTo(song.getSongName());
}
return 0;
}
}
//歌手
public class Singer implements Comparable{
private String name;
private Song song;
public Singer() {
super();
}
public Singer(String name) {
super();
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Song getSong() {
return song;
}
public void setSong(Song song) {
this.song = song;
}
@Override
public String toString() {
return name;
}
@Override
public int compareTo(Object o) {
if(o == this){
return 0;
}
if(o instanceof Singer){
Singer singer = (Singer)o;
return name.compareTo(singer.getName());
}
return 0;
}
}

Map 实现类之二:LinkedHashMap

• LinkedHashMap 是 HashMap 的子类

• 存储数据采用的哈希表结构+链表结构,在 HashMap 存储结构的基础上,使用了一对 双向链表来记录添加元素的先后顺序,可以保证遍历元素时,与添加的顺序一致。

• 通过哈希表结构可以保证键的唯一、不重复,需要键所在类重写 hashCode()方法、 equals()方法

public class TestLinkedHashMap {
 public static void main(String[] args) {
 LinkedHashMap map = new LinkedHashMap();
 map.put("王五", 13000.0);
 map.put("张三", 10000.0);
 //key 相同,新的 value 会覆盖原来的 value
 //因为 String 重写了 hashCode 和 equals 方法
 map.put("张三", 12000.0);
 map.put("李四", 14000.0);
 //HashMap 支持 key 和 value 为 null 值
 String name = null;
 Double salary = null;
 map.put(name, salary);
 Set entrySet = map.entrySet();
 for (Object obj : entrySet) {
 Map.Entry entry = (Map.Entry)obj;
 System.out.println(entry);
 }
 }
}

 

Map 实现类之三:TreeMap

TreeMap 存储 key-value 对时,需要根据 key-value 对进行排序。TreeMap 可以保 证所有的 key-value 对处于有序状态。

• TreeSet 底层使用红黑树结构存储数据

• TreeMap 的 Key 的排序:

 – 自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所 有的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException – 定制排序:创建 TreeMap 时,构造器传入一个 Comparator 对象,该对 象负责对 TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现 Comparable 接口

• TreeMap 判断两个 key 相等的标准:两个 key 通过 compareTo()方法或者 compare() 方法返回 0。

/*
 * 自然排序举例
 * */
 @Test
 public void test1(){
 TreeMap map = new TreeMap();
 map.put("CC",45);
 map.put("MM",78);
 map.put("DD",56);
 map.put("GG",89);
 map.put("JJ",99);
 Set entrySet = map.entrySet();
 for(Object entry : entrySet){
 System.out.println(entry);
 }
 }

 定制排序

@Test
 public void test2(){
 //按照 User 的姓名的从小到大的顺序排列
 TreeMap map = new TreeMap(new Comparator() {
 @Override
 public int compare(Object o1, Object o2) {
 if(o1 instanceof User && o2 instanceof User){
 User u1 = (User)o1;
 User u2 = (User)o2;
 return u1.name.compareTo(u2.name);
 }
 throw new RuntimeException("输入的类型不匹配");
 }
 });
 map.put(new User("Tom",12),67);
 map.put(new User("Rose",23),"87");
 map.put(new User("Jerry",2),88);
 map.put(new User("Eric",18),45);
 map.put(new User("Tommy",44),77);
 map.put(new User("Jim",23),88);
 map.put(new User("Maria",18),34);
 Set entrySet = map.entrySet();
 for(Object entry : entrySet){
 System.out.println(entry);
 }
 }
}
class User implements Comparable{
 String name;
 int age;
 public User(String name, int age) {
 this.name = name;
 this.age = age;
 }
 public User() {
 }
 @Override
 public String toString() {
 return "User{" +
 "name='" + name + '\'' +
 ", age=" + age +
 '}';
 }
 /*
 举例:按照 age 从小到大的顺序排列,如果 age 相同,则按照 name 从大到小的
顺序排列
 * */
 @Override
 public int compareTo(Object o) {
 if(this == o){
 return 0;
 }
 if(o instanceof User){
 User user = (User)o;
 int value = this.age - user.age;
 if(value != 0){
 return value;
 }
 return -this.name.compareTo(user.name);
 }
 throw new RuntimeException("输入的类型不匹配");
 }
}

Map 实现类之四:Hashtable

• Hashtable 是 Map 接口的古老实现类,JDK1.0 就提供了。不同于 HashMap, Hashtable 是线程安全的。

• Hashtable 实现原理和 HashMap 相同,功能相同。底层都使用哈希表结构(数组+单 向链表),查询速度快。

• 与 HashMap 一样,Hashtable 也不能保证其中 Key-Value 对的顺序

• Hashtable 判断两个 key 相等、两个 value 相等的标准,与 HashMap 一致。

• 与 HashMap 不同,Hashtable 不允许使用 null 作为 key 或 value。

Hashtable 和 HashMap 的区别

HashMap:底层是一个哈希表(jdk7:数组+链表;jdk8:数组+链表+红黑树),是一个线 程不安全的集合,执行效率高

Hashtable:底层也是一个哈希表(数组+链表),是一个线程安全的集合,执行效率低

HashMap 集合:可以存储 null 的键、null 的值

Hashtable 集合,不能存储 null 的键、null 的值

Hashtable 和 Vector 集合一样,在 jdk1.2 版本之后被更先进的集合(HashMap,Arra yList)取代了。所以 HashMap 是 Map 的主要实现类,Hashtable 是 Map 的古老实现 类。

Hashtable 的子类 Properties(配置文件)依然活跃在历史舞台

Properties 集合是一个唯一和 IO 流相结合的集合

Map 实现类之五:Properties 

• Properties 类是 Hashtable 的子类,该对象用于处理属性文件

.• 由于属性文件里的 key、value 都是字符串类型,所以 Properties 中要求 key 和 value 都是字符串类型.

• 存取数据时,建议使用 setProperty(String key,String value)方法和 getProperty(String key)方法

 @Test
    public void test01() {
        Properties properties = System.getProperties();
        String fileEncoding = properties.getProperty("file.encoding");//当前源文件字符编码
        System.out.println("fileEncoding = " + fileEncoding); //fileEncoding = UTF-8
    }
    @Test
    public void test02() {
        Properties properties = new Properties();
        properties.setProperty("user","songhk");
        properties.setProperty("password","123456");
        System.out.println(properties);//{password=123456, user=songhk}
    }
    @Test
    public void test03() throws IOException {
        Properties pros = new Properties();
        pros.load(new FileInputStream("D:\\test\\JAVASE_Code\\Java_Demo03\\src\\jdbc.properties"));
        String user = pros.getProperty("user");
        System.out.println(user); //sad
    }

jdbc.properties文件

user=sad

Collections 工具类

• reverse(List):反转 List 中元素的顺序

• shuffle(List):对 List 集合元素进行随机排序

• sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序

• sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行 排序

• swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换

• reverse(List):反转 List 中元素的顺序

• shuffle(List):对 List 集合元素进行随机排序

• sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序

• sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行 排序

• swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换

• void copy(List dest,List src):将 src 中的内容复制到 dest 中

• boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象 的所有旧值

• 提供了多个 unmodifiableXxx()方法,该方法返回指定 Xxx 的不可修改的视图。

• boolean addAll(Collection c,T... elements)将所有指定元素添加到指定 collection 中。

• Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集合包装成 线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题

泛型 

在 Java 中,我们在声明方法时,当在完成方法功能时如果有未知的数据需要参 与,这些未知的数据需要在调用方法时才能确定,那么我们把这样的数据通过 形参表示。在方法体中,用这个形参名来代表那个未知的数据,而调用者在调 用时,对应的传入实参就可以了。

集合类在设计阶段/声明阶段不能确定这个容器到底实际存的是什么类型的对 象,所以在 JDK5.0 之前只能把元素类型设计为 Object,JDK5.0 时 Java 引入了 “参数化类型(Parameterized type)”的概念,允许我们在创建集合时指定集 合元素的类型。比如:List,这表明该 List 只能保存字符串类型的对 象。

java.lang.Comparable 接口和 java.util.Comparator 接口,是用于比较对象 大小的接口。这两个接口只是限定了当一个对象大于另一个对象时返回正整 数,小于返回负整数,等于返回 0,但是并不确定是什么类型的对象比较大 小。JDK5.0 之前只能用 Object 类型表示,使用时既麻烦又不安全,因此 JDK5.0 给它们增加了泛型。

其中<T>就是类型参数,即泛型。 所谓泛型,就是允许在定义类、接口时通过一个标识表示类中某个属 性的类型或者是某个方法的返回值或参数的类型。这个类型参数将在使 用时(例如,继承或实现这个接口、创建对象或调用方法时)确定 (即传入实际的类型参数,也称为类型实参)。  

package map;

import java.util.Comparator;

public class Circle {
    private double radius;
    public Circle(double radius) {
        super();
        this.radius = radius;
    }
    public double getRadius() {
        return radius;
    }
    public void setRadius(double radius) {
        this.radius = radius;
    }
    @Override
    public String toString() {
        return "Circle [radius=" + radius + "]";
    }
}
class m implements Comparator<Circle> {

    @Override
    public int compare(Circle o1, Circle o2) {
        return Double.compare(o1.getRadius(), o2.getRadius());
    }
}
class te {
    public static void main(String[] args) {
        m com = new m();
        System.out.println(com.compare(new Circle(2), new Circle
                (1)));
    }
}

List list = new ArrayList<>(); //类型推断

泛型,也称为泛型参数,即参数的类型,只能使用引用数据类型进行赋值。(不能使 用基本数据类型,可以使用包装类替换)

• 集合声明时,声明泛型参数。在使用集合时,可以具体指明泛型的类型。一旦指明, 类或接口内部,凡是使用泛型参数的位置,都指定为具体的参数类型。如果没有指明 的话,看做是 Object 类型。 

自定义泛型结构

<类型>这种语法形式就叫泛型。

在哪里可以声明类型变量<T>

声明类或接口时,在类名或接口名后面声明泛型类型,我们把这样的类或接口称为泛 型类或泛型接口。

【修饰符】 class 类名 <类型变量列表>【extends 父类】 【implements 接口 们】{ }

【修饰符】 interface 接口名<类型变量列表> 【implements 接口们】{ }

声明方法时,在【修饰符】与返回值类型之间声明类型变量,我们把声明了类型变量 的方法,称为泛型方法。

[修饰符] <类型变量列表>返回值类型 方法名([形参列表])[throws 异常列表]{ //...

//例如:
public class ArrayList<E> 
public interface Map<K,V>{
 ....
} 
//例如:java.util.Arrays 类中的
public static <T> List<T> asList(T... a){
 ....
}

自定义泛型类或泛型接口

当我们在类或接口中定义某个成员时,该成员的相关类型是不确定的,而这个 类型需要在使用这个类或接口时才可以确定,那么我们可以使用泛型类、泛型 接口。

举例

class Person<T> {
 // 使用 T 类型定义变量
 private T info;
 // 使用 T 类型定义一般方法
 public T getInfo() {
 return info;
 }
 public void setInfo(T info) {
 this.info = info;
 }
 // 使用 T 类型定义构造器
 public Person() {
 }
 public Person(T info) {
 this.info = info;
 }
 // static 的方法中不能声明泛型
 //public static void show(T t) {
 //
 //}
 // 不能在 try-catch 中使用泛型定义
 //public void test() {
 //try {
 //
 //} catch (MyException<T> ex) {
 //
 //}
 //}
}

 练习2 

class Student<T>{
 private String name;
 private T score;
 public Student() {
 super();
 }
 public Student(String name, T score) {
 super();
 this.name = name;
 this.score = score;
 }
 public String getName() {
 return name;
 }
 public void setName(String name) {
 this.name = name;
 }
 public T getScore() {
 return score;
 }
 public void setScore(T score) {
 this.score = score;
 }
 @Override
 public String toString() {
 return "姓名:" + name + ", 成绩:" + score;
 }
}
public class TestStudent {
 public static void main(String[] args) {
 //语文老师使用时:
 Student<String> stu1 = new Student<String>("张三", "良好");
 //数学老师使用时:
 //Student<double> stu2 = new Student<double>("张三", 90.5);//
错误,必须是引用数据类型
 Student<Double> stu2 = new Student<Double>("张三", 90.5);
 //英语老师使用时:
 Student<Character> stu3 = new Student<Character>("张三", 'C');
 //错误的指定
 //Student<Object> stu = new Student<String>();//错误的
 }
}

自定义泛型方法

泛型方法的格式:[访问权限] <泛型> 返回值类型 方法名([泛型标识 参数名称]) [抛出的异常]{}

public class DAO {
 public <E> E get(int id, E e) {
 E result = null;
 return result;
 }
}

 举例二

public static <T> void fromArrayToCollection(T[] a, Collection<T> c)
{
 for (T o : a) {
 c.add(o);
 }
}
public static void main(String[] args) {
 Object[] ao = new Object[100];
 Collection<Object> co = new ArrayList<Object>();
 fromArrayToCollection(ao, co);
 String[] sa = new String[20];
 Collection<String> cs = new ArrayList<>();
 fromArrayToCollection(sa, cs);
 Collection<Double> cd = new ArrayList<>();
 // 下面代码中 T 是 Double 类,但 sa 是 String 类型,编译错误。
 // fromArrayToCollection(sa, cd);
 // 下面代码中 T 是 Object 类型,sa 是 String 类型,可以赋值成功。
 fromArrayToCollection(sa, co);
}

通配符的使用

        当我们声明一个变量/形参时,这个变量/形参的类型是一个泛型类或泛型接 口,例如:Comparator 类型,但是我们仍然无法确定这个泛型类或泛型接口的 类型变量的具体类型,此时我们考虑使用类型通配符 ? 。

使用类型通配符:? 比如:List<?>,Map<?>

.List<?>是 List<String>、List<Object>等各种泛型 List 的父类。 

通配符的读与写

写操作:

将任意元素加入到其中不是类型安全的: Collection<?> c = new ArrayList();

c.add(new Object()); // 编译时错误

因为我们不知道 c 的元素类型,我们不能向其中添加对象。add 方法有类型参 数 E 作为集合的元素类型。我们传给 add 的任何参数都必须是一个未知类型的 子类。因为我们不知道那是什么类型,所以我们无法传任何东西进去。 唯一可以插入的元素是 null,因为它是所有引用类型的默认值。

 读操作:

另一方面,读取 List<?>的对象 list 中的元素时,永远是安全的,因为不管 list 的真实类型是什么,它包含的都是 Object。

public class TestWildcard {
 public static void m4(Collection<?> coll){
 for (Object o : coll) {
 System.out.println(o);
 }
 }
}

public class test {
    public static void main(String[] args) {
        List<?> list = null;
        list = new ArrayList<String>();
        list = new ArrayList<Double>();
     //list.add("3"); // 编译报错
        list.add(null);
        List<String> l1 = new ArrayList<String>();
        List<Integer> l2 = new ArrayList<Integer>();
        l1.add("asd");
        l2.add(15);
        read(l1);
        read(l2);
    }
    public static void read(List<?> list) {
        for (Object o : list) {
            System.out.println(o);
        }
    }

File 类与 IO 流

File

• 一个 File 对象代表硬盘或网络中可能存在的一个文件或者文件目录(俗称文件夹), 与平台无关。(体会万事万物皆对象)

• File 能新建、删除、重命名文件和目录,但 File 不能访问文件内容本身。如果需要 访问文件内容本身,则需要使用输入/输出流。

        – File 对象可以作为参数传递给流的构造器

• 想要在 Java 程序中表示一个真实存在的文件或目录,那么必须有一个 File 对象,但是 Java 程序中的一个 File 对象,可能没有一个真实存在的文件或目录。

• File 类及本章下的各种流,都定义在 java.io 包下。

路径

• 绝对路径:从盘符开始的路径,这是一个完整的路径。

• 相对路径:相对于项目目录的路径,这是一个便捷的路径,开发中经常使用。

        – IDEA 中,main 中的文件的相对路径,是相对于"当前工程"

        – IDEA 中,单元测试方法中的文件的相对路径,是相对于"当前 module" 

常用方法 

• public String getName() :获取名称

• public String getPath() :获取路径

• public String getAbsolutePath():获取绝对路径

• public File getAbsoluteFile():获取绝对路径表示的文件

• public String getParent():获取上层文件目录路径。若无,返回 null

• public long length() :获取文件长度(即:字节数)。不能获取目录的长度。

• public String[] list() :返回一个 String 数组,表示该 File 目录中的所有子文件或目 录

• public File[] listFiles() :返回一个 File 数组,表示该 File 目录中的所有的子文件或目 录。

• public boolean exists() :此 File 表示的文件或目录是否实际存在。

• public boolean isDirectory() :此 File 表示的是否为目录。

• public boolean isFile() :此 File 表示的是否为文件。

• public boolean canRead() :判断是否可读

• public boolean canWrite() :判断是否可写

• public boolean isHidden() :判断是否隐藏

 举例:列出D盘下java目录中所有的文件与目录

    @Test
    public void test01() {
        File dir = new File("d:/java");
        String[] subs = dir.list();
        for (String sub : subs) {
            System.out.println(sub);
        }
    }

创建、删除功能

        • public boolean createNewFile() :创建文件。若文件存在,则不创建,返回 false。

• public boolean mkdir() :创建文件目录。如果此文件目录存在,就不创建了。 如果此文件目录的上层目录不存在,也不创建。

• public boolean mkdirs() :创建文件目录。如果上层文件目录不存在,一并创 建。

• public boolean delete() :删除文件或者文件夹 删除注意事项:① Java 中的 删除不走回收站。② 要删除一个文件目录,请注意该文件目录内不能包含文件或者 文件目录。

  File f = new File("aaa.txt");
        System.out.println("aaa.txt 是否存在:"+f.exists());
        System.out.println("aaa.txt 是否创建:"+f.createNewFile());
        System.out.println("aaa.txt 是否存在:"+f.exists());
        // 目录的创建
        File f2= new File("newDir");
        System.out.println("newDir 是否存在:"+f2.exists());
        System.out.println("newDir 是否创建:"+f2.mkdir());
        System.out.println("newDir 是否存在:"+f2.exists());
        // 创建一级目录
        File f3= new File("newDira\\newDirb");
        System.out.println("newDira\\newDirb 创建:" + f3.mkdir());
        File f4= new File("newDir\\newDirb");
        System.out.println("newDir\\newDirb 创建:" + f4.mkdir());
        // 创建多级目录
        File f5= new File("newDira\\newDirb");
        System.out.println("newDira\\newDirb 创建:" + f5.mkdirs());
        // 文件的删除
      //  System.out.println("aaa.txt 删除:" + f.delete());
        // 目录的删除
       System.out.println("newDir 删除:" + f2.delete());
     System.out.println("newDir\\newDirb 删除:" + f4.delete());

 遍历指定目录所有文件名称,包括子文件目录中的文件。(方式一)

     @Test
    public void test(){
            File dir=new File("D:/java");
      printSubFile(dir);
}
    public static void printSubFile(File dir) {
        File[] subfiles = dir.listFiles();
        for (File f : subfiles) {
            if (f.isDirectory()) {// 文件目录
                printSubFile(f);
            } else {// 文件
                System.out.println(f.getAbsolutePath());
            }
        }
    }

  遍历指定目录所有文件名称,包括子文件目录中的文件。(方式二)

    @Test
    public void test1(){
        File dir=new File("D:/java");

        listAllSubFiles(dir);
    }
    public void listAllSubFiles(File file) {
        if (file.isFile()) {
            System.out.println(file);
        } else {
            File[] all = file.listFiles();
            // 如果 all[i]是文件,直接打印
            // 如果 all[i]是目录,接着再获取它的下一级
            for (File f : all) {
                listAllSubFiles(f);// 递归调用:自己调用自己就叫递归
            }
        }
    }

IO 流原理及流的分类

• Java 程序中,对于数据的输入/输出操作以“流(stream)” 的方式进行,可以看做是 一种数据的流动。

• I/O 流中的 I/O 是 Input/Output 的缩写, I/O 技术是非常实用的技术,用于处理设 备之间的数据传输。如读/写文件,网络通讯等。

        – 输入 input:读取外部数据(磁盘、光盘等存储设备的数据)到程序(内 存)中。

        – 输出 output:将程序(内存)数据输出到磁盘、光盘等存储设备中。

 流的分类

按数据的流向不同分为:输入流和输出流。

– 输入流 :把数据从其他设备上读取到内存中的流。

        • 以 InputStream、Reader 结尾

– 输出流 :把数据从内存 中写出到其他设备上的流。

        • 以 OutputStream、Writer 结尾

• 按操作数据单位的不同分为:字节流(8bit)和字符流(16bit) 

– 字节流 :以字节为单位,读写数据的流。

        • 以 InputStream、OutputStream 结尾

– 字符流 :以字符为单位,读写数据的流。

        • 以 Reader、Writer 结尾

• 根据 IO 流的角色不同分为:节点流和处理流。

– 节点流:直接从数据源或目的地读写数据

– 处理流:不直接连接到数据源或目的地,而是“连接”在已存在的流(节点 流或处理流)之上,通过对数据的处理为程序提供更为强大的读写功能

流的 API

• Java 的 IO 流共涉及 40 多个类,实际上非常规则,都是从如下 4 个抽象基类派生的。

常用的节点流:

• 文件流: FileInputStream、FileOutputStrean、FileReader、FileWriter

• 字节/字符数组流: ByteArrayInputStream、ByteArrayOutputStream、 CharArrayReader、CharArrayWriter

– 对数组进行处理的节点流(对应的不再是文件,而是内存中的一个数组)。

常用处理流:

• 缓冲流:BufferedInputStream、BufferedOutputStream、BufferedReader、 BufferedWriter

– 作用:增加缓冲功能,避免频繁读写硬盘,进而提升读写效率。

• 转换流:InputStreamReader、OutputStreamReader

– 作用:实现字节流和字符流之间的转换。

• 对象流:ObjectInputStream、ObjectOutputStream

– 作用:提供直接读写 Java 对象功能

Reader 与 Writer

ava 提供一些字符流类,以字符为单位读写数据,专门用于处理文本文件。不 能操作图片,视频等非文本文件。

常见的文本文件有如下的格式:.txt、.java、.c、.cpp、.py 等

注意:.doc、.xls、.ppt 这些都不是文本文件。

字符输入流:Reader

常用方法

• public int read(): 从输入流读取一个字符。 虽然读取了一个字符,但是会自 动提升为 int 类型。返回该字符的 Unicode 编码值。如果已经到达流末尾了,则返回-1。

• public int read(char[] cbuf): 从输入流中读取一些字符,并将它们存储到 字符数组 cbuf 中 。每次最多读取 cbuf.length 个字符。返回实际读取的字符个数。 如果已经到达流末尾,没有数据可读,则返回-1。

• public int read(char[] cbuf,int off,int len):从输入流中读取一些字 符,并将它们存储到字符数组 cbuf 中,从 cbuf[off]开始的位置存储。每次最多读取 len 个字符。返回实际读取的字符个数。如果已经到达流末尾,没有数据可读,则返 回-1。

• public void close() :关闭此流并释放与此流相关联的任何系统资源。 注意:当完成流的操作时,必须调用 close()方法,释放系统资源,否 则会造成内存泄漏。

字符输出流:Writer

常用方法

• public void write(int c) :写出单个字符。

• public void write(char[] cbuf):写出字符数组。

• public void write(char[] cbuf, int off, int len):写出字符数组的某 一部分。off:数组的开始索引;len:写出的字符个数。

• public void write(String str):写出字符串。

• public void write(String str, int off, int len) :写出字符串的某一 部分。off:字符串的开始索引;len:写出的字符个数。

• public void flush():刷新该流的缓冲。

• public void close() :关闭此流。

注意:当完成流的操作时,必须调用 close()方法,释放系统资源,否 则会造成内存泄漏。

FileReader 与 FileWriter

FileReader

java.io.FileReader 类用于读取字符文件,构造时使用系统默认的字符编码和默认字节缓冲区。

• FileReader(File file): 创建一个新的 FileReader ,给定要读取的 File 对象。

• FileReader(String fileName): 创建一个新的 FileReader ,给定要读取的文 件的名称。

举例:读取 hello.txt 文件中的字符数据,并显示在控制台上

    @Test
    public void test3() throws IOException {
        //1. 创建 File 类的对象,对应着物理磁盘上的某个文件
        File file = new File("D:\\test\\JAVASE_Code\\hello");
        //2. 创建 FileReader 流对象,将 File 类的对象作为参数传递到 FileReader 的构造器中
        FileReader fr = new FileReader(file);
        //3. 通过相关流的方法,读取文件中的数据
// int data = fr.read(); //每调用一次读取一个字符
// while (data != -1) {
// System.out.print((char) data);
// data = fr.read();
// }
        int data;
        while ((data = fr.read()) != -1) {
            System.out.print((char) data);
        }
        //4. 关闭相关的流资源,避免出现内存泄漏
        fr.close();
    }

实现方式 2:在方式 1 的基础上改进,使用 try-catch-finally 处理异常。 保证流是可以关闭的

 @Test
    public void test3() throws IOException {
        FileReader fr = null;
        try {
            //1. 创建 File 类的对象,对应着物理磁盘上的某个文件
            File file = new File("D:\\test\\JAVASE_Code\\hello");
            //2. 创建 FileReader 流对象,将 File 类的对象作为参数传递到 FileReader 的构造器中
            fr = new FileReader(file);
            //3. 通过相关流的方法,读取文件中的数据
            /*
             * read():每次从对接的文件中读取一个字符。并将此字符返回。
             * 如果返回值为-1,则表示文件到了末尾,可以不再读取。
             * */
// int data = fr.read();
// while(data != -1){
// System.out.print((char)data);
// data = fr.read();
// }
            int data;
            while ((data = fr.read()) != -1) {
                System.out.print((char) data);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //4. 关闭相关的流资源,避免出现内存泄漏
            try {
                if (fr != null)
                    fr.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

实现方式 3:调用 read(char[] cbuf),每次从文件中读取多个字符

    @Test
    public void test3() {
        FileReader fr = null;
        try {
            //1. 创建 File 类的对象,对应着物理磁盘上的某个文件
            File file = new File("D:\\test\\JAVASE_Code\\hello");
            //2. 创建 FileReader 流对象,将 File 类的对象作为参数传递到 FileReader 的构造器中
            fr = new FileReader(file);
            //3. 通过相关流的方法,读取文件中的数据
            char[] cbuf = new char[5];
 /*
 * read(char[] cbuf) : 每次将文件中的数据读入到 cbuf 数组中,
并返回读入到数组中的
 * 字符的个数。
 * */
            int len; //记录每次读入的字符的个数
            while ((len = fr.read(cbuf)) != -1) {
                //处理 char[]数组即可
                //错误:
// for(int i = 0;i < cbuf.length;i++){
// System.out.print(cbuf[i]);
// }
                //错误:
// String str = new String(cbuf);
// System.out.print(str);
                //正确:
// for(int i = 0;i < len;i++){
// System.out.print(cbuf[i]);
// }
                //正确:
                String str = new String(cbuf, 0, len);
                System.out.print(str);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //4. 关闭相关的流资源,避免出现内存泄漏
            try {
                if (fr != null)
                    fr.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

 FileWriter

java.io.FileWriter 类用于写出字符到文件,构造时使用系统默认的字符编码 和默认字节缓冲区。

• FileWriter(File file): 创建一个新的 FileWriter,给定要读取的 File 对象。

• FileWriter(String fileName): 创建一个新的 FileWriter,给定要读取的文件 的名称。

• FileWriter(File file,boolean append): 创建一个新的 FileWriter,指明是 否在现有文件末尾追加内容。

举例:

    @Test
    public void test3() throws IOException {

        //注意:应该使用 try-catch-finally 处理异常。这里出于方便阅读代码,使用了 throws 的方式

            // 使用文件名称创建流对象
            FileWriter fw = new FileWriter(new File("D:\\test\\JAVASE_Code\\fw.txt"));
            // 写出数据
            fw.write(97); // 写出第 1 个字符
            fw.write('b'); // 写出第 2 个字符
            fw.write('C'); // 写出第 3 个字符
            fw.write(30000); // 写出第 4 个字符,中文编码表中 30000 对应一个汉字。
            //关闭资源
            fw.close();
        }

 

 方式二

    @Test
    public void test3() throws IOException {


            // 使用文件名称创建流对象
            FileWriter fw = new FileWriter(new File("D:\\test\\JAVASE_Code\\fw.txt"));
            // 字符串转换为字节数组
            char[] chars = "你好,世界".toCharArray();
            // 写出字符数组
            fw.write(chars);
            // 写出从索引 1 开始,2 个字符。
            fw.write(chars,1,2); // 好,
            // 关闭资源
            fw.close();
        }

方式三:

  @Test
    public void test3() throws IOException {


        FileWriter fw = null;
        try {
            //1. 创建 File 的对象
            File file = new File("D:\\test\\JAVASE_Code\\fw.txt");
            //2. 创建 FileWriter 的对象,将 File 对象作为参数传递到 FileWriter 的构造器中
            //如果输出的文件已存在,则会对现有的文件进行覆盖
            fw = new FileWriter(file);
// fw = new FileWriter(file,false);//如果输出的文件已存在,则会在现有的文件末尾写入数据
// fw = new FileWriter(file,true);
            //3. 调用相关的方法,实现数据的写出操作
            //write(String str) / write(char[] cbuf)
            fw.write("I love you,");
            fw.write("you love him.");
            fw.write("so sad".toCharArray());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //4. 关闭资源,避免内存泄漏
            try {
                if (fw != null)
                    fw.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

 小结

① 因为出现流资源的调用,为了避免内存泄漏,需要使用 try-catch-finally 处理异 常

② 对于输入流来说,File 类的对象必须在物理磁盘上存在,否则执行就会报 FileNotFo undException。如果传入的是一个目录,则会报 IOException 异常。

对于输出流来说,File 类的对象是可以不存在的。

> 如果 File 类的对象不存在,则可以在输出的过程中,自动创建 File 类的对象

> 如果 File 类的对象存在,

> 如果调用 FileWriter(File file)或 FileWriter(File file,false), 输出时会新建 File 文件覆盖已有的文件

> 如果调用 FileWriter(File file,true)构造器,则在现有的文件末尾追加 写出内容。

关于 flush(刷新)

因为内置缓冲区的原因,如果 FileWriter 不关闭输出流,无法写出字符到文件 中。但是关闭的流对象,是无法继续写出数据的。如果我们既想写出数据,又 想继续使用流,就需要 flush() 方法了。

• flush() :刷新缓冲区,流对象可以继续使用。

• close():先刷新缓冲区,然后通知系统释放资源。流对象不可以再被使用了。

注意:即便是 flush()方法写出了数据,操作的最后还是要调用 close 方法,释放 系统资源。

    @Test
    public void test3() throws IOException {


        //注意:应该使用 try-catch-finally 处理异常。这里出于方便阅读代码,使用了 throws 的方式

            // 使用文件名称创建流对象
            FileWriter fw = new FileWriter("D:\\test\\JAVASE_Code\\fw.txt");
            // 写出数据,通过 flush
            fw.write('刷'); // 写出第 1 个字符
            fw.flush();
            fw.write('新'); // 继续写出第 2 个字符,写出成功
            fw.flush();
            // 写出数据,通过 close
            fw.write('关'); // 写出第 1 个字符
            fw.close();
           // fw.write('闭'); // 继续写出第 2 个字符,【报错】java.io.IOException: Stream closed
            fw.close();

        }

节点流之二:FileInputStream\FileOutputStream 

如果我们读取或写出的数据是非文本文件,则 Reader、Writer 就无能为力了, 必须使用字节流。

字节输入流:InputStream 

• public int read(byte[] b): 从输入流中读取一些字节数,并将它们存储到字 节数组 b 中 。每次最多读取 b.length 个字节。返回实际读取的字节个数。如果已经 到达流末尾,没有数据可读,则返回-1。

• public int read(byte[] b,int off,int len):从输入流中读取一些字节 数,并将它们存储到字节数组 b 中,从 b[off]开始存储,每次最多读取 len 个字节 。 返回实际读取的字节个数。如果已经到达流末尾,没有数据可读,则返回-1。

方法和字符流类似

字节输出流:OutputStream

• public void write(int b) :将指定的字节输出流。虽然参数为 int 类型四个字 节,但是只会保留一个字节的信息写出。

• public void write(byte[] b):将 b.length 字节从指定的字节数组写入此输出 流。

• public void write(byte[] b, int off, int len) :从指定的字节数组写 入 len 字节,从偏移量 off 开始输出到此输出流。

方法和字符流类似

FileInputStream 与 FileOutputStream

FileInputStream

java.io.FileInputStream 类是文件输入流,从文件中读取字节。

举例

fw文件内容是helloa

  @Test
    public void test3() throws IOException {
        // 使用文件名称创建流对象
        FileInputStream fis = new FileInputStream("D:\\test\\JAVASE_Code\\fw.txt");
        // 读取数据,返回一个字节
        int read = fis.read();
        System.out.print((char) read);
        read = fis.read();
        System.out.print((char) read);
        read = fis.read();
        System.out.print((char) read);
        read = fis.read();
        System.out.print((char) read);
        read = fis.read();
        System.out.print((char) read);
        // 读取到末尾,返回-1
        read = fis.read();
        System.out.print(read);
        // 关闭资源
        fis.close();
}

方式二

@Test
 public void test02()throws IOException{
 // 使用文件名称创建流对象
 FileInputStream fis = new FileInputStream("D:\test\JAVASE_Code\fw.txt");
 // 定义变量,保存数据
 int b;
 // 循环读取
 while ((b = fis.read())!=-1) {
 System.out.println((char)b);
 }
 // 关闭资源
 fis.close();
 }

方式三

@Test
 public void test03()throws IOException{
 // 使用文件名称创建流对象.
 FileInputStream fis = new FileInputStream("D:\test\JAVASE_Code\fw.txt"); // 文件
中为 abcde
 // 定义变量,作为有效个数
 int len;
 // 定义字节数组,作为装字节数据的容器
 byte[] b = new byte[2];
 // 循环读取
 while (( len= fis.read(b))!=-1) {
 // 每次读取后,把数组变成字符串打印
 System.out.println(new String(b));
 }
 // 关闭资源
 fis.close();

 }

方式四(推荐)

@Test
 public void test04()throws IOException{
 // 使用文件名称创建流对象.
 FileInputStream fis = new FileInputStream("D:\test\JAVASE_Code\fw.txt"); // 文件
中为 abcde
 // 定义变量,作为有效个数
 int len;
 // 定义字节数组,作为装字节数据的容器
 byte[] b = new byte[2];
 // 循环读取
 while (( len= fis.read(b))!=-1) {
 // 每次读取后,把数组的有效字节部分,变成字符串打印
 System.out.println(new String(b,0,len));// len 每次读取的
有效字节个数
 }
 // 关闭资源
 fis.close();

 }

 FileOutputStream

java.io.FileOutputStream 类是文件输出流,用于将数据写出到文件。

  @Test
    public void test3() throws IOException {
        // 使用文件名称创建流对象
        File fis = new File("D:\\test\\JAVASE_Code\\fw.txt");

            // 使用文件名称创建流对象
            FileOutputStream fos = new FileOutputStream(fis,true);//默认false
            // 字符串转换为字节数组
            byte[] b = "abcde".getBytes();
            // 写出从索引 2 开始,2 个字节。索引 2 是 c,两个字节,也就是 cd。
            fos.write(b,2,2);
            // 关闭资源
            fos.close();
        }

/使用 FileInputStream\FileOutputStream,实现对文件的复制 

@Test
    public void test3() throws IOException {
        // 使用文件名称创建流对象
     //   File fis = new File("D:\\test\\JAVASE_Code\\fw.txt");
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            //1. 造文件-造流
            //复制图片:成功
// fis = new FileInputStream(new File("pony.jpg"));
// fos = new FileOutputStream(new File("pony_copy1.jpg"));
            //复制文本文件:成功
            fis = new FileInputStream(new File("D:\\\\test\\\\JAVASE_Code\\\\fw.txt"));
            fos = new FileOutputStream(new File("D:\\\\test\\\\JAVASE_Code\\\\fffff.txt"));
            //2. 复制操作(读、写)
            byte[] buffer = new byte[1024];
            int len;//每次读入到 buffer 中字节的个数
            while ((len = fis.read(buffer)) != -1) {
                fos.write(buffer, 0, len);
// String str = new String(buffer,0,len);
// System.out.print(str);
            }
            System.out.println("复制成功");
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            //3. 关闭资源
            try {
                if (fos != null)
                    fos.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            try {
                if (fis != null)
                    fis.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

 练习:图片的加密解密

加密

 @Test
    public void test3() throws IOException {
        /*
         * 图片的加密
         * */

            FileInputStream fis = null;
            FileOutputStream fos = null;
            try {
                File file1 = new File("C:\\Users\\Administrator\\Pictures\\Screenshots\\1.png");
                File file2 = new File("D:\\test\\JAVASE_Code\\1.png");
                fis = new FileInputStream(file1);
                fos = new FileOutputStream(file2);
                //方式 1:每次读入一个字节,效率低
// int data;
// while((data = fis.read()) != -1){
// fos.write(data ^ 5);
// }
                //方式 2:每次读入一个字节数组,效率高
                int len;
                byte[] buffer = new byte[1024];
                while ((len = fis.read(buffer)) != -1) {
                    for (int i = 0; i < len; i++) {
                        buffer[i] = (byte) (buffer[i] ^ 5);
                    }
                    fos.write(buffer, 0, len);
                }
                System.out.println("加密成功");
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

解密

  @Test
    public void test4(){
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            File file1 = new File("D:\\test\\JAVASE_Code\\1.png");
            File file2 = new File("D:\\test\\JAVASE_Code\\2.png");
            fis = new FileInputStream(file1);
            fos = new FileOutputStream(file2);
            //方式 1:每次读入一个字节,效率低
// int data;
// while((data = fis.read()) != -1){
// fos.write(data ^ 5);
// }
            //方式 2:每次读入一个字节数组,效率高
            int len;
            byte[] buffer = new byte[1024];
            while((len = fis.read(buffer)) != -1){
                for(int i = 0;i < len;i++){
                    buffer[i] = (byte) (buffer[i] ^ 5);
                }
                fos.write(buffer,0,len);
            }
            System.out.println("解密成功");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

 处理流之一:缓冲流

• 为了提高数据读写的速度,Java API 提供了带缓冲功能的流类:缓冲流。

• 缓冲流要“套接”在相应的节点流之上,根据数据操作单位可以把缓冲流分为:

– 字节缓冲流:BufferedInputStream,BufferedOutputStream

– 字符缓冲流:BufferedReader,BufferedWriter

• 缓冲流的基本原理:在创建流对象时,内部会创建一个缓冲区数组(缺省使用 8192 个字节(8Kb)的缓冲区),通过缓冲区读写,减少系统 IO 次数,从而提高读写的效 率。

构造器

• public BufferedInputStream(InputStream in) :创建一个 新的字节型的缓 冲输入流。

• public BufferedOutputStream(OutputStream out): 创建一个新的字节型 的缓冲输出流。

// 创建字节缓冲输入流
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("abc.jpg"));
// 创建字节缓冲输出流
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("abc_copy.jpg"));

• public BufferedReader(Reader in) :创建一个 新的字符型的缓冲输入流。

• public BufferedWriter(Writer out): 创建一个新的字符型的缓冲输出流。 

// 创建字符缓冲输入流
BufferedReader br = new BufferedReader(new FileReader("br.txt"));
// 创建字符缓冲输出流
BufferedWriter bw = new BufferedWriter(new FileWriter("bw.txt"));

效率测试 

首先用字节流复制一个文件看看需要多长时间

文件大小

 public void test3() throws IOException {
            FileInputStream fis = null;
            FileOutputStream fos = null;
            try {
                File file1 = new File("E:\\游览器下载\\1.exe");
                File file2 = new File("E:\\游览器下载\\2.exe");
                fis = new FileInputStream(file1);
                fos = new FileOutputStream(file2);
                int len;
                byte[] buffer = new byte[1024];
                while ((len = fis.read(buffer)) != -1) {
                    for (int i = 0; i < len; i++) {
                        buffer[i] = (byte) (buffer[i] ^ 5);
                    }
                    fos.write(buffer, 0, len);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
   @Test
    public void test5() throws IOException {
       long l = System.currentTimeMillis();
       test3();
       long l1 = System.currentTimeMillis();
       System.out.println("需要时间"+(l1-l)+"毫秒");

   }

使用缓冲流来复制

  public void test3() throws IOException {
        BufferedOutputStream bos=null;
         BufferedInputStream bis=null;
            FileInputStream fis = null;
            FileOutputStream fos = null;
            try {
                File file1 = new File("E:\\游览器下载\\1.exe");
                File file2 = new File("E:\\游览器下载\\3.exe");
                bis=new BufferedInputStream(new FileInputStream(file1));
                bos=new BufferedOutputStream(new FileOutputStream(file2));
                int len;
                byte[] buffer = new byte[1024];
                while ((len = bis.read(buffer)) != -1) {
                    for (int i = 0; i < len; i++) {
                        buffer[i] = (byte) (buffer[i] ^ 5);
                    }
                    bos.write(buffer, 0, len);
                }
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                   bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                try {
                   bis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
   @Test
    public void test5() throws IOException {
       long l = System.currentTimeMillis();
       test3();
       long l1 = System.currentTimeMillis();
       System.out.println("需要时间"+(l1-l)+"毫秒");

   }

 

字符缓冲流特有方法

字符缓冲流的基本方法与普通字符流调用方式一致,不再阐述,我们来看它们 具备的特有方法。

• BufferedReader:public String readLine(): 读一行文字。

• BufferedWriter:public void newLine(): 写一行行分隔符,由系统属性定义符 号。

要读取的文件

 

   @Test
    public void test5() throws IOException {

           // 创建流对象
           BufferedReader br = new BufferedReader(new FileReader("D:\\test\\JAVASE_Code\\fw.txt "));
                   // 定义字符串,保存读取的一行文字
                   String line;
           // 循环读取,读取到最后返回 null
           while ((line = br.readLine())!=null) {
               System.out.println(line);
           }
           // 释放资源
           br.close();

   }

 

 第二个方法测试

   @Test
    public void test5() throws IOException {

           // 创建流对象
           BufferedReader br = new BufferedReader(new FileReader("D:\\test\\JAVASE_Code\\fw.txt "));
       // 创建流对象
       BufferedWriter bw = new BufferedWriter(new FileWriter("D:\\test\\JAVASE_Code\\fw.txt"));
               // 写出数据
               bw.write("你");
       // 写出换行
       bw.newLine();
       bw.write("好");
       bw.newLine();
       bw.write("!");
       bw.newLine();
       // 释放资源
       bw.close();
   }

说明:

涉及到嵌套的多个流时,如果都显式关闭的话,需要先关闭外层的 流,再关闭内层的流

其实在开发中,只需要关闭最外层的流即可,因为在关闭外层流 时,内层的流也会被关闭。 

处理流之二:转换流

使用 FileReader 读取项目中的文本文件。由于 IDEA 设置中针对项目设置了 UTF-8 编码,当读取 Windows 系统中创建的文本文件时,如果 Windows 系统 默认的是 GBK 编码,则读入内存中会出现乱码。

public class Problem {
 public static void main(String[] args) throws IOException {
 FileReader fileReader = new FileReader("E:\\File_GBK.txt");
 int data;
 while ((data = fileReader.read()) != -1) {
 System.out.print((char)data);
 }
 fileReader.close();
 }

针对文本文件,现在使用一个字节流进行数据的读入,希望将数据显示在控制 台上。此时针对包含中文的文本数据,可能会出现乱码

转换流作用:转换流是字节与字符间的桥梁!

InputStreamReader 与 OutputStreamWriter 

InputStreamReader

– 转换流 java.io.InputStreamReader,是 Reader 的子类,是从字节流 到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它 的字符集可以由名称指定,也可以接受平台的默认字符集。

– 构造器

• InputStreamReader(InputStream in): 创建一个使用默认字 符集的字符流。

• InputStreamReader(InputStream in, String charsetName): 创建一个指定字符集的字符流。

 文件内容

@Test
    public void test5() throws IOException {

       // 定义文件路径,文件为 gbk 编码
       String fileName = "E:\\游览器下载\\ANSI.txt";
       //方式 1:
       // 创建流对象,默认 UTF8 编码
       InputStreamReader isr1 = new InputStreamReader(new FileInputStream(fileName));
       // 定义变量,保存字符
       int charData;
       // 使用默认编码字符流读取,乱码
       while ((charData = isr1.read()) != -1) {
           System.out.print((char)charData); // ��Һ�
       }
       isr1.close();
       System.out.println();
       //方式 2:
       // 创建流对象,指定 GBK 编码
       InputStreamReader isr2 = new InputStreamReader(new FileInputStream(fileName) , "gbk");
       // 使用指定编码字符流读取,正常解析
       while ((charData = isr2.read()) != -1) {
           System.out.print((char)charData);// 
       }
       isr2.close();
   }

 

OutputStreamWriter 

– 转换流 java.io.OutputStreamWriter ,是 Writer 的子类,是从字符 流到字节流的桥梁。使用指定的字符集将字符编码为字节。它的字符集可 以由名称指定,也可以接受平台的默认字符集。

– 构造器

• OutputStreamWriter(OutputStream in): 创建一个使用默认 字符集的字符流。

• OutputStreamWriter(OutputStream in,String charsetName): 创建一个指定字符集的字符流。

 @Test
    public void test5() throws IOException {


       String fileName = "E:\\游览器下载\\1.txt";
       OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(fileName));
       // 写出数据
       osw.write("你好"); // 保存为 6 个字节
       osw.close();
       // 定义文件路径
       String FileName2 = "E:\\游览器下载\\2.txt";
       // 创建流对象,指定 GBK 编码
       OutputStreamWriter osw2 = new OutputStreamWriter
               (new FileOutputStream(FileName2),"GBK");
       // 写出数据
       osw2.write("你好");// 保存为 4 个字节
       osw2.close();

   }

处理流之三/四:数据流、对象流

如果需要将内存中定义的变量(包括基本数据类型或引用数据类型)保存在文 件中,那怎么办呢?

int age = 300;

char gender = '男';

int energy = 5000;

double price = 75.5;

boolean relive = true;

String name = "巫师";

Student stu = new Student("张三",23,89);

Java 提供了数据流和对象流来处理这些类型的数据:

数据流:DataOutputStream、DataInputStream

– DataOutputStream:允许应用程序将基本数据类型、String 类型的变量写 入输出流中

– DataInputStream:允许应用程序以与机器无关的方式从底层输入流中读取 基本数据类型、String 类型的变量。

• 对象流 DataInputStream 中的方法

byte readByte()

short readShort()

等等

• 对象流 DataOutputStream 中的方法:将上述的方法的 read 改为相应的 write 即可。

• 数据流的弊端:只支持 Java 基本数据类型和字符串的读写,而不支持其它 Java 对象 的类型。而 ObjectOutputStream 和 ObjectInputStream 既支持 Java 基本数据类型的 数据读写,又支持 Java 对象的读写,所以重点介绍对象流 ObjectOutputStream 和 ObjectInputStream。

• 对象流:ObjectOutputStream、ObjectInputStream

– ObjectOutputStream:将 Java 基本数据类型和对象写入字节输出流中。 通过在流中使用文件可以实现 Java 各种基本数据类型的数据以及对象的持 久存储。

– ObjectInputStream:ObjectInputStream 对以前使用 ObjectOutputStream 写出的基本数据类型的数据和对象进行读入操作,保存在内存中。

说明:对象流的强大之处就是可以把 Java 中的对象写入到数据源中,

也能把对象从数据源中还原回来。 

ObjectOutputStream 中的构造器:

public ObjectOutputStream(OutputStream out): 创建一个指定的 ObjectOutputStream。

FileOutputStream fos = new FileOutputStream("game.dat");
ObjectOutputStream oos = new ObjectOutputStream(fos);

 ObjectOutputStream 中的方法:

• public void writeBoolean(boolean val):写出一个 boolean 值。 其他类似i

public void writeObject(Object obj):写出一个 obj 对象

• public void writeUTF(String str):将表示长度信息的两个字节写入输出流,后跟字符串 s 中每个字符的 UTF-8 修改版表示形式。根据字符的值,将字符串 s 中每个字符转 换成一个字节、两个字节或三个字节的字节组。注意,将 String 作为基本数据写入 流中与将它作为 Object 写入流中明显不同。 如果 s 为 null,则抛出 NullPointerException。

ObjectInputStream 中的构造器:

FileInputStream fis = new FileInputStream("game.dat");
ObjectInputStream ois = new ObjectInputStream(fis);

方法参考ObjectOutputStream

认识对象序列化机制

对象序列化机制允许把内存中的 Java 对象转换成平台无关的二进制流,从而允 许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另 一个网络节点。//当其它程序获取了这种二进制流,就可以恢复成原来的 Java 对象。

        序列化过程:用一个字节序列可以表示一个对象,该字节序列包含该对象的类型和 对象中存储的属性等信息。字节序列写出到文件之后,相当于文件中持久保存了一 个对象的信息。

        反序列化过程:该字节序列还可以从文件中读取回来,重构对象,对它进行反序列 化。对象的数据、对象的类型和对象中存储的数据信息,都可以用来在内存中创建 对象。

 

序列化是 RMI(Remote Method Invoke、远程方法调用)过程的参数和返回值 都必须实现的机制,而 RMI 是 JavaEE 的基础。因此序列化机制是 JavaEE 平 台的基础。

序列化的好处,在于可将任何实现了 Serializable 接口的对象转化为字节数据, 使其在保存和传输时可被还原。

实现原理

序列化:用 ObjectOutputStream 类保存基本类型数据或对象的机制。方法为:

– public final void writeObject (Object obj) : 将指定的对象写 出。

反序列化:用 ObjectInputStream 类读取基本类型数据或对象的机制。方法为: – public final Object readObject () : 读取一个对象。

如何实现序列化机制

如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序 列化的,为了让某个类是可序列化的,该类必须实现 java.io.Serializable 接口。Serializable 是一个标记接口,不实现此接口的类将不会使任何状态序 列化或反序列化,会抛出 NotSerializableException 。

• 如果对象的某个属性也是引用数据类型,那么如果该属性也要序列化的话,也要实现 Serializable 接口

• 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必 须注明是瞬态的,使用 transient 关键字修饰。

• 静态(static)变量的值不会序列化。因为静态变量的值不属于某个对象。 

 方式一

  @Test
    public void save() throws IOException {
        String name = "巫师";
        int age = 300;
        char gender = '男';
        int energy = 5000;
        double price = 75.5;
        boolean relive = true;
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\\游览器下载\\game.dat"));
        oos.writeUTF(name);
        oos.writeInt(age);
        oos.writeChar(gender);
        oos.writeInt(energy);
        oos.writeDouble(price);
        oos.writeBoolean(relive);
        oos.close();
    }
    @Test
    public void reload()throws IOException{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("E:\\游览器下载\\game.dat"));
        String name = ois.readUTF();
        int age = ois.readInt();
        char gender = ois.readChar();
        int energy = ois.readInt();
        double price = ois.readDouble();
        boolean relive = ois.readBoolean();
        System.out.println(name+"," + age + "," + gender + "," + energy + "," + price + "," + relive);
        ois.close();
    }

 举例二

public class Employee implements Serializable {
 //static final long serialVersionUID = 23234234234L;
 public static String company; //static 修饰的类变量,不会被序列化
 public String name;
 public String address;
 public transient int age; // transient 瞬态修饰成员,不会被序列化
 public Employee(String name, String address, int age) {
 this.name = name;
 this.address = address;
 this.age = age;
 }
//get  set 简化
 }

@Test
 public void save() throws IOException {
 Employee.setCompany("你好");
 Employee e = new Employee("小谷姐姐", "宏福苑", 23);
 // 创建序列化流对象
 ObjectOutputStream oos = new ObjectOutputStream(new FileOutpu
tStream("employee.dat"));
 // 写出对象
 oos.writeObject(e);
 // 释放资源
 oos.close();
 System.out.println("Serialized data is saved"); // 姓名,地址
被序列化,年龄没有被序列化。
 }
@Test
 public void reload() throws IOException, ClassNotFoundException {
 // 创建反序列化流
 FileInputStream fis = new FileInputStream("employee.dat");
 ObjectInputStream ois = new ObjectInputStream(fis);
 // 读取一个对象
 Employee e = (Employee) ois.readObject();
 // 释放资源
 ois.close();
 fis.close();
 System.out.println(e);
 }
}

举例三

:如果有多个对象需要序列化,则可以将对象放到集合中,再序列化集 合对象即可。(这里用的还是上一个建立的实体类)

@Test
 public void save() throws IOException {
 ArrayList<Employee> list = new ArrayList<>();
 list.add(new Employee("张三", "宏福苑", 23));
 list.add(new Employee("李四", "白庙", 24));
 list.add(new Employee("王五", "平西府", 25));
 // 创建序列化流对象
 ObjectOutputStream oos = new ObjectOutputStream(new FileOutpu
tStream("employees.dat"));
 // 写出对象
 oos.writeObject(list);
 // 释放资源
 oos.close();
 }
 @Test
 public void reload() throws IOException, ClassNotFoundException {
 // 创建反序列化流
 FileInputStream fis = new FileInputStream("employees.dat");
 ObjectInputStream ois = new ObjectInputStream(fis);
 // 读取一个对象
 ArrayList<Employee> list = (ArrayList<Employee>) ois.readObje
ct();
 // 释放资源
 ois.close();
 fis.close();
 System.out.println(list);
 }
}

反序列化失败问题

问题 1:

对于 JVM 可以反序列化对象,它必须是能够找到 class 文件的类。如果找不到 该类的 class 文件,则抛出一个 ClassNotFoundException 异常。

问题 2:

当 JVM 反序列化对象时,能找到 class 文件,但是 class 文件在序列化对象之后 发生了修改,那么反序列化操作也会失败,抛出一个 InvalidClassException 异常。发生这个异常的原因如下:

• 该类的序列版本号与从流中读取的类描述符的版本号不匹配

• 该类包含未知数据类型

解决办法:

Serializable 接口给需要序列化的类,提供了一个序列版本号:

serialVersionUID 。凡是实现 Serializable 接口的类都应该有一个表示序列化 版本标识符的静态变量:

static final long serialVersionUID = 234242343243L; //它的值由程序员随
意指定即可。

 • serialVersionUID 用来表明类的不同版本间的兼容性。简单来说,Java 的序列化机制 是通过在运行时判断类的 serialVersionUID 来验证版本一致性的。在进行反序列化 时,JVM 会把传来的字节流中的 serialVersionUID 与本地相应实体类的 serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就 会出现序列化版本不一致的异常(InvalidCastException)。

• 如果类没有显示定义这个静态常量,它的值是 Java 运行时环境根据类的内部细节自 动生成的。若类的实例变量做了修改,serialVersionUID 可能发生变化。因此,建议 显式声明。

• 如果声明了 serialVersionUID,即使在序列化完成之后修改了类导致类重新编译,则 原来的数据也能正常反序列化,只是新增的字段值是默认值而已。

public class Employee implements Serializable {
 private static final long serialVersionUID = 1324234L; //增加 seri
alVersionUID
 
 //其它结构:略
}

其他流的使用 。

标准输入、输出流

• System.in 和 System.out 分别代表了系统标准的输入和输出设备

• 默认输入设备是:键盘,输出设备是:显示器

• System.in 的类型是 InputStream

• System.out 的类型是 PrintStream,其是 OutputStream 的子类 FilterOutputStream 的 子类

• 重定向:通过 System 类的 setIn,setOut 方法对默认设备进行改变

– public static void setIn(InputStream in)

– public static void setOut(PrintStream out)

从键盘输入字符串,要求将读取到的整行字符串转成大写输出。然后继续进行 输入操作,直至当输入“e”或者“exit”时,退出程序

  System.out.println("请输入信息(退出输入 e 或 exit):");
// 把"标准"输入流(键盘输入)这个字节流包装成字符流,再包装成缓冲流
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String s = null;
        try {
            while ((s = br.readLine()) != null) { // 读取用户输入的一行数据 -->阻塞程序
                if ("e".equalsIgnoreCase(s) || "exit".equalsIgnoreCase(s)) {
                    System.out.println("安全退出!!");
                    break;
                }
                // 将读取到的整行字符串转成大写输出
                System.out.println("-->:" + s.toUpperCase());
                System.out.println("继续输入信息");
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (br != null) {
                    br.close(); // 关闭过滤流时,会自动关闭它包装的底层节点流
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

        }

打印流

实现将基本数据类型的数据格式转化为字符串输出

• 打印流:PrintStream 和 PrintWriter

– PrintStream 和 PrintWriter 的输出不会抛出 IOException 异常

– PrintStream 和 PrintWriter 有自动 flush 功能

– PrintStream 打印的所有字符都使用平台的默认字符编码转换为字节。在需 要写入字符而不是写入字节的情况下,应该使用 PrintWriter 类。

– System.out 返回的是 PrintStream 的实例

• 构造器

– PrintStream(File file) :创建具有指定文件且不带自动行刷新的新打印流。

– PrintStream(File file, String csn):创建具有指定文件名称和字符集且不带自 动行刷新的新打印流。

– PrintStream(OutputStream out) :创建新的打印流。

– PrintStream(OutputStream out, boolean autoFlush):创建新的打印流。 autoFlush 如果为 true,则每当写入 byte 数组、调用其中一个 println 方 法或写入换行符或字节 ('\n') 时都会刷新输出缓冲区。

– PrintStream(OutputStream out, boolean autoFlush, String encoding) :创建 新的打印流。 – PrintStream(String fileName):创建具有指定文件名称且不带自动行刷新的 新打印流。

– PrintStream(String fileName, String csn) :创建具有指定文件名称和字符集 且不带自动行刷新的新打印流。

 没有文件会自动创建文件

     PrintStream ps = new PrintStream("D:\\test\\JAVASE_Code\\fw.txt");
            ps.println("hello");
            ps.println(1);
            ps.println(1.5);
            ps.close();

Scanner 类

Scanner(File source) :构造一个新的 Scanner,它生成的值是从指定文件扫描的。

Scanner(File source, String charsetName) :构造一个新的 Scanner,它生成的值是从 指定文件扫描的。

• Scanner(InputStream source) :构造一个新的 Scanner,它生成的值是从指定的输入 流扫描的。

• Scanner(InputStream source, String charsetName) :构造一个新的 Scanner,它生成 的值是从指定的输入流扫描的。

常用方法:

• boolean hasNextXxx(): 如果通过使用 nextXxx()方法,此扫描器输入信息中的下一个 标记可以解释为默认基数中的一个 Xxx 值,则返回 true。

• Xxx nextXxx(): 将输入信息的下一个标记扫描为一个 Xx

举例一 

    Scanner input = new Scanner(System.in);
        PrintStream ps = new PrintStream("D:\\test\\JAVASE_Code\\fw.txt");
        while(true){
            System.out.print("请输入一个单词:");
            String str = input.nextLine();
            if("stop".equals(str)){
                break;
            }
            ps.println(str);
        }
        input.close();
        ps.close();

 举例二

   Scanner inputs = new Scanner(new FileInputStream("D:\\test\\JAVASE_Code\\fw.txt"));
        while(inputs.hasNextLine()){
            String str = inputs.nextLine();
            System.out.println(str);
        }
        inputs.close();

网络编程

Java 是 Internet 上的语言,它从语言级上提供了对网络应用程序的支持,程序 员能够很容易开发常见的网络应用程序。

Java 提供的网络类库,可以实现无痛的网络连接,联网的底层细节被隐藏在 Java 的本机安装系统里,由 JVM 进行控制。并且 Java 实现了一个跨平台的 网络库,程序员面对的是一个统一的网络编程环境。

• C/S 架构 :全称为 Client/Server 结构,是指客户端和服务器结构。常见程序有 QQ、 美团 app、360 安全卫士等软件。

B/S 架构 :全称为 Browser/Server 结构,是指浏览器和服务器结构。常见浏览 器有 IE、谷歌、火狐等。

 IP 地址 指互联网协议地址(Internet Protocol Address),俗称 IP

公网地址( 万维网使用)和 私有地址( 局域网使用)。

192.168.开头的就是私 有地址,范围即为 192.168.0.0--192.168.255.255,专门为组织机构内部使用。

特殊的 IP 地址:

• 本地回环地址(hostAddress):127.0.0.1

• 主机名(hostName):localhost

 网络编程 API

InetAddress 类主要表示 IP 地址,两个子类:Inet4Address、Inet6Address。 InetAddress 类没有提供公共的构造器,而是提供 了 如下几个 静态方法来获 取 InetAddress 实例

• public static InetAddress getLocalHost()

• public static InetAddress getByName(String host)

• public static InetAddress getByAddress(byte[] addr)

InetAddress 提供了如下几个常用的方法

• public String getHostAddress() :返回 IP 地址字符串(以文本表现形式)

• public String getHostName() :获取此 IP 地址的主机名

•public boolean isReachable(int timeout):测试是否可以达到该地址

    @Test
    public void test01() throws UnknownHostException{
        InetAddress localHost = InetAddress.getLocalHost();
        System.out.println(localHost);
    }
    @Test
    public void test02()throws UnknownHostException{
        InetAddress atguigu = InetAddress.getByName("www.baidu.com ");
                System.out.println(atguigu);
    }
    @Test
    public void test03()throws UnknownHostException {
// byte[] addr = {112,54,108,98};
        byte[] addr = {(byte)192,(byte)168,24,56};
        InetAddress atguigu = InetAddress.getByAddress(addr);
        System.out.println(atguigu);
    }

 Socket 类

ServerSocket 类的构造方法:

• ServerSocket(int port) :创建绑定到特定端口的服务器套接字。

ServerSocket 类的常用方法:

• Socket accept():侦听并接受到此套接字的连接。

Socket 类的常用构造方法:

• public Socket(InetAddress address,int port):创建一个流套接字并将其连接到指定 IP 地址的指定端口号。

• public Socket(String host,int port):创建一个流套接字并将其连接到指定主机上的指 定端口号。

Socket 类的常用方法:

• public InputStream getInputStream():返回此套接字的输入流,可以用于接收消息

• public OutputStream getOutputStream():返回此套接字的输出流,可以用于发送消 息

• public InetAddress getInetAddress():此套接字连接到的远程 IP 地址;如果套接字是 未连接的,则返回 null。

• public InetAddress getLocalAddress():获取套接字绑定的本地地址。

• public int getPort():此套接字连接到的远程端口号;如果尚未连接套接字,则返回 0。

• public int getLocalPort():返回此套接字绑定到的本地端口。如果尚未绑定套接字,则 返回 -1。 • public void close():关闭此套接字。套接字被关闭后,便不可在以后的网络连接中使 用(即无法重新连接或重新绑定)。需要创建新的套接字对象。 关闭此套接字也将会 关闭该套接字的 InputStream 和 OutputStream。

• public void shutdownInput():如果在套接字上调用 shutdownInput() 后从套接字输入 流读取内容,则流将返回 EOF(文件结束符)。 即不能在从此套接字的输入流中接收 任何数据。

• public void shutdownOutput():禁用此套接字的输出流。对于 TCP 套接字,任何以 前写入的数据都将被发送,并且后跟 TCP 的正常连接终止序列。 如果在套接字上调 用 shutdownOutput() 后写入套接字输出流,则该流将抛出 IOException。 即不能通 过此套接字的输出流发送任何数据。

注意:先后调用 Socket 的 shutdownInput()和 shutdownOutput()方法,仅仅关 闭了输入流和输出流,并不等于调用 Socket 的 close()方法。在通信结束后,仍 然要调用 Scoket 的 close()方法,因为只有该方法才会释放 Socket 占用的资 源,比如占用的本地端口号等。 

 DatagramSocket 类

DatagramPacket 类

自行查阅

TCP编程

开发步骤

        客户端程序包含以下四个基本的步骤 :

• 创建 Socket :根据指定服务端的 IP 地址或端口号构造 Socket 类对象。若服务器 端响应,则建立客户端到服务器的通信线路。若连接失败,会出现异常。

• 打开连接到 Socket 的输入/ 出流: 使用 getInputStream()方法获得输入流,使用 getOutputStream()方法获得输出流,进行数据传输

• 按照一定的协议对 Socket 进行读/ 写操作:通过输入流读取服务器放入线路的信息 (但不能读取自己放入线路的信息),通过输出流将信息写入线路。

• 关闭 Socket :断开客户端到服务器的连接,释放线路 

服务器端程序包含以下四个基本的 步骤:

• 调用 ServerSocket(int port) :创建一个服务器端套接字,并绑定到指定端口上。用 于监听客户端的请求。

• 调用 accept() :监听连接请求,如果客户端请求连接,则接受连接,返回通信套接 字对象。

• 调用 该 Socket 类对象的 getOutputStream() 和 getInputStream () :获取输出流和 输入流,开始网络数据的发送和接收。

• 关闭 Socket 对象:客户端访问结束,关闭通信套接字。 

  //服务端
    public static void main(String[] args)throws Exception {
        //1、准备一个 ServerSocket 对象,并绑定 8888 端口
        ServerSocket server = new ServerSocket(8888);
        System.out.println("等待连接....");
        //2、在 8888 端口监听客户端的连接,该方法是个阻塞的方法,如果没有客户端连接,将一直等待
        Socket socket = server.accept();
        InetAddress inetAddress = socket.getInetAddress();
        System.out.println(inetAddress.getHostAddress() + "客户端连接成功!!");
                //3、获取输入流,用来接收该客户端发送给服务器的数据
                InputStream input = socket.getInputStream();
        //接收数据
        byte[] data = new byte[1024];
        StringBuilder s = new StringBuilder();
        int len;
        while ((len = input.read(data)) != -1) {
            s.append(new String(data, 0, len));
        }
        System.out.println(inetAddress.getHostAddress() + "客户端发送的消息是:" + s);
                //4、获取输出流,用来发送数据给该客户端
                OutputStream out = socket.getOutputStream();
        //发送数据
        out.write("欢迎登录".getBytes());
        out.flush();
        //5、关闭 socket,不再与该客户端通信
        //socket 关闭,意味着 InputStream 和 OutputStream 也关闭了
        socket.close();
        //6、如果不再接收任何客户端通信,可以关闭 ServerSocket
        server.close();
    }

 

//客户端
    public static void main(String[] args) throws Exception {
        // 1、准备 Socket,连接服务器,需要指定服务器的 IP 地址和端口号
        Socket socket = new Socket("127.0.0.1", 8888);
        // 2、获取输出流,用来发送数据给服务器
        OutputStream out = socket.getOutputStream();
        // 发送数据
        out.write("lalala".getBytes());
        //会在流末尾写入一个“流的末尾”标记,对方才能读到-1,否则对方的读取方法会一致阻塞
        socket.shutdownOutput();
        //3、获取输入流,用来接收服务器发送给该客户端的数据
        InputStream input = socket.getInputStream();
        // 接收数据
        byte[] data = new byte[1024];
        StringBuilder s = new StringBuilder();
        int len;
        while ((len = input.read(data)) != -1) {
            s.append(new String(data, 0, len));
        }
        System.out.println("服务器返回的消息是:" + s);
        //4、关闭 socket,不再与服务器通信,即断开与服务器的连接
        //socket 关闭,意味着 InputStream 和 OutputStream 也关闭了
        socket.close();
    }

注意要先启动服务端然后才能启动客户端 

如果先启动客户端则报ConnectException

案例需求:多个客户端连接服务器,并进行多次通信

 • 每一个客户端连接成功后,从键盘输入英文单词或中国成语,并发送给服务器

• 服务器收到客户端的消息后,把词语“反转”后返回给客户端

• 客户端接收服务器返回的“词语”,打印显示

• 当客户端输入“stop”时断开与服务器的连接

• 多个客户端可以同时给服务器发送“词语”,服务器可以“同时”处理多个客户端的请求

 

package arrdess;

import java.io.*;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

public class A {
    //服务端
    public static void main(String[] args)throws Exception {
        // 1、准备一个 ServerSocket
        ServerSocket server = new ServerSocket(8888);
        System.out.println("等待连接...");
        int count = 0;
        while(true){
            // 2、监听一个客户端的连接
            Socket socket = server.accept();
            System.out.println("第" + ++count + "个客户端"+socket.getInetAddress().getHostAddress()+"连接成功!!");
            ClientHandlerThread ct = new ClientHandlerThread(socket);
            ct.start();
        }
        //这里没有关闭 server,永远监听
    }
    static class ClientHandlerThread extends Thread{
        private Socket socket;
        private String ip;
        public ClientHandlerThread(Socket socket) {
            super();
            this.socket = socket;
            ip = socket.getInetAddress().getHostAddress();
        }
        public void run(){
            try{
                //(1)获取输入流,用来接收该客户端发送给服务器的数据
                BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                //(2)获取输出流,用来发送数据给该客户端
                PrintStream ps = new PrintStream(socket.getOutputStream());
                String str;
                // (3)接收数据
                while ((str = br.readLine()) != null) {
                    //(4)反转
                    StringBuilder word = new StringBuilder(str);
                    word.reverse();
                    //(5)返回给客户端
                    ps.println(word);
                }
                System.out.println("客户端" + ip+"正常退出");
            }catch(Exception e){
                System.out.println("客户端" + ip+"意外退出");
            }finally{
                try {
                    //(6)断开连接
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}
package arrdess;

import java.io.*;
import java.net.Socket;
import java.util.Scanner;

public class B {
    //客户端
    public static void main(String[] args) throws Exception {
        // 1、准备 Socket,连接服务器,需要指定服务器的 IP 地址和端口号
        Socket socket = new Socket("127.0.0.1", 8888);
        // 2、获取输出流,用来发送数据给服务器
        OutputStream out = socket.getOutputStream();
        PrintStream ps = new PrintStream(out);
        // 3、获取输入流,用来接收服务器发送给该客户端的数据
        InputStream input = socket.getInputStream();
        BufferedReader br;
        if(args!= null && args.length>0) {
            String encoding = args[0];
            br = new BufferedReader(new InputStreamReader(input,encoding));
        }else{
            br = new BufferedReader(new InputStreamReader(input));
        }
        Scanner scanner = new Scanner(System.in);
        while(true){
            System.out.println("输入发送给服务器的单词或成语:");
            String message = scanner.nextLine();
            if(message.equals("stop")){
                socket.shutdownOutput();
                break;
            }
            // 4、 发送数据
            ps.println(message);
            // 接收数据
            String feedback = br.readLine();
            System.out.println("从服务器收到的反馈是:" + feedback);
        }
        //5、关闭 socket,断开与服务器的连接
        scanner.close();
        socket.close();

    }
}

案例:聊天室

package arrdess;

import java.io.*;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;

public class A {
    //这个集合用来存储所有在线的客户端
    static ArrayList<Socket> online = new ArrayList<Socket>();

    public static void main(String[] args) throws Exception {
//1、启动服务器,绑定端口号
        ServerSocket server = new ServerSocket(8989);
//2、接收 n 多的客户端同时连接
        while (true) {
            Socket accept = server.accept();
            online.add(accept);//把新连接的客户端添加到 online 列表中
            MessageHandler mh = new MessageHandler(accept);
            mh.start();//
        }
    }

    static class MessageHandler extends Thread {
        private Socket socket;
        private String ip;

        public MessageHandler(Socket socket) {
            super();
            this.socket = socket;
        }

        public void run() {
            try {
                ip = socket.getInetAddress().getHostAddress();
//插入:给其他客户端转发“我上线了”
                sendToOther(ip + "上线了");
//(1)接收该客户端的发送的消息
                InputStream input = socket.getInputStream();
                InputStreamReader reader = new InputStreamReader(input);
                BufferedReader br = new BufferedReader(reader);
                String str;
                while ((str = br.readLine()) != null) {
//(2)给其他在线客户端转发
                    sendToOther(ip + ":" + str);
                }
                sendToOther(ip + "下线了");
            } catch (IOException e) {
                try {
                    sendToOther(ip + "掉线了");
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            } finally {
//从在线人员中移除我
                online.remove(socket);
            }
        }

        //封装一个方法:给其他客户端转发 xxx 消息
        public void sendToOther(String message) throws IOException {
//遍历所有的在线客户端,一一转发
            for (Socket on : online) {
                OutputStream every = on.getOutputStream();
//为什么用 PrintStream?目的用它的 println 方法,按行打印
                PrintStream ps = new PrintStream(every);
                ps.println(message);
            }
        }

    }
}
package arrdess;

import java.io.*;
import java.net.Socket;
import java.util.Scanner;

public class B {
    public static void main(String[] args)throws Exception {
//1、连接服务器
        Socket socket = new Socket("127.0.0.1",8989);
//2、开启两个线程
//(1)一个线程负责看别人聊,即接收服务器转发的消息
        Receive receive = new Receive(socket);
        receive.start();
//(2)一个线程负责发送自己的话
        Send send = new Send(socket);
        send.start();
        send.join();//等我发送线程结束了,才结束整个程序
        socket.close();
    }
}
class Send extends Thread{
    private Socket socket;
    public Send(Socket socket) {
        super();
        this.socket = socket;
    }
    public void run(){
        try {
            OutputStream outputStream = socket.getOutputStream();
//按行打印
            PrintStream ps = new PrintStream(outputStream);
            Scanner input = new Scanner(System.in);
//从键盘不断的输入自己的话,给服务器发送,由服务器给其他人转发
            while(true){
                System.out.print("自己的话:");
                String str = input.nextLine();
                if("bye".equals(str)){
                    break;
                }
                ps.println(str);
            }
            input.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
class Receive extends Thread{
    private Socket socket;
    public Receive(Socket socket) {
        super();
        this.socket = socket;
    }
    public void run(){
        try {
            InputStream inputStream = socket.getInputStream();
            Scanner input = new Scanner(inputStream);
            while(input.hasNextLine()){
                String line = input.nextLine();
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

UDP 网络编程

UDP(User Datagram Protocol,用户数据报协议):是一个无连接的传输层协 议、提供面向事务的简单不可靠的信息传送服务,类似于短信。

开发步骤

发送端程序包含以下四个基本的步骤:

• 创建 DatagramSocket :默认使用系统随机分配端口号。

• 创建 DatagramPacket:将要发送的数据用字节数组表示,并指定要发送的数据长 度,接收方的 IP 地址和端口号。

• 调用 该 DatagramSocket 类对象的 send 方法 :发送数据报 DatagramPacket 对象。

• 关闭 DatagramSocket 对象:发送端程序结束,关闭通信套接字。

接收端程序包含以下四个基本的步骤 :

• 创建 DatagramSocket :指定监听的端口号。

• 创建 DatagramPacket:指定接收数据用的字节数组,起到临时数据缓冲区的效果, 并指定最大可以接收的数据长度。

• 调用 该 DatagramSocket 类对象的 receive 方法 :接收数据报 DatagramPacket 对 象。。 • 关闭 DatagramSocket :接收端程序结束,关闭通信套接字。

举例 1:

发送端:

DatagramSocket ds = null;
try {
 ds = new DatagramSocket();
 byte[] by = "hello,atguigu.com".getBytes();
 DatagramPacket dp = new DatagramPacket(by, 0, by.length, InetA
ddress.getByName("127.0.0.1"), 10000);
 ds.send(dp);
} catch (Exception e) {
 e.printStackTrace();
} finally {
 if (ds != null)
 ds.close();
}

 接收端:

DatagramSocket ds = null;
try {
 ds = new DatagramSocket(10000);
 byte[] by = new byte[1024*64];
 DatagramPacket dp = new DatagramPacket(by, by.length);
 ds.receive(dp);
 String str = new String(dp.getData(), 0, dp.getLength());
 System.out.println(str + "--" + dp.getAddress());
} catch (Exception e) {
 e.printStackTrace();
} finally {
 if (ds != null)
 ds.close();
}

举例2

发送端:

public static void main(String[] args)throws Exception {
// 1、建立发送端的 DatagramSocket
 DatagramSocket ds = new DatagramSocket();
 //要发送的数据
 ArrayList<String> all = new ArrayList<String>();
 all.add("尚硅谷让天下没有难学的技术!");
 all.add("学高端前沿的 IT 技术来尚硅谷!");
 all.add("尚硅谷让你的梦想变得更具体!");
 all.add("尚硅谷让你的努力更有价值!");
 //接收方的 IP 地址
 InetAddress ip = InetAddress.getByName("127.0.0.1");
 //接收方的监听端口号
 int port = 9999;
 //发送多个数据报
 for (int i = 0; i < all.size(); i++) {
// 2、建立数据包 DatagramPacket
 byte[] data = all.get(i).getBytes();
 DatagramPacket dp = new DatagramPacket(data, 0, data.lengt
h, ip, port);
// 3、调用 Socket 的发送方法
 ds.send(dp);
 }
// 4、关闭 Socket
 ds.close();
 }

发送端:

public static void main(String[] args) throws Exception {
// 1、建立接收端的 DatagramSocket,需要指定本端的监听端口号
 DatagramSocket ds = new DatagramSocket(9999);
 //一直监听数据
 while(true){
 //2、建立数据包 DatagramPacket
 byte[] buffer = new byte[1024*64];
 DatagramPacket dp = new DatagramPacket(buffer,buffer.lengt
h);
 //3、调用 Socket 的接收方法
 ds.receive(dp);
 //4、拆封数据
 String str = new String(dp.getData(),0,dp.getLength());
 System.out.println(str);
 }
// ds.close();
 }

反射机制

ava 程序中,所有的对象都有两种类型:编译时类型和运行时类型,而很多时候 对象的编译时类型和运行时类型不一致。 Object obj = new String("hello"); obj.getClass()

例如:某些变量或形参的声明类型是 Object 类型,但是程序却需要调用该对象 运行时类型的方法,该方法不是 Object 中的方法,那么如何解决呢? 解决这个问题,有两种方案:

方案 1:在编译和运行时都完全知道类型的具体信息,在这种情况下,我们可 以直接先使用 instanceof 运算符进行判断,再利用强制类型转换符将其转换成 运行时类型的变量即可。 

方案 2:编译时根本无法预知该对象和类的真实信息,程序只能依靠运行时信息 来发现该对象和类的真实信息,这就必须使用反射。

反射概述

Reflection(反射)是被视为动态语言的关键,反射机制允许程序在运行期间借助 于 Reflection API 取得任何类的内部信息,并能直接操作任意对象的内部属性及 方法。

加载完类之后,在堆内存的方法区中就产生了一个 Class 类型的对象(一个类 只有一个 Class 对象),这个对象就包含了完整的类的结构信息。我们可以通 过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构, 所以,我们形象的称之为:反射。 

 

Java 反射机制研究及应用

Java 反射机制提供的功能:

• 在运行时判断任意一个对象所属的类

• 在运行时构造任意一个类的对象

• 在运行时判断任意一个类所具有的成员变量和方法

• 在运行时获取泛型信息

• 在运行时调用任意一个对象的成员变量和方法

• 在运行时处理注解

• 生成动态代理

反射相关的主要 API

 java.lang.Class:代表一个类

 java.lang.reflect.Method:代表类的方法

java.lang.reflect.Field:代表类的成员变量

java.lang.reflect.Constructor:代表类 的构造器 … …

反射的优缺点 

优点

• 提高了 Java 程序的灵活性和扩展性,降低了耦合性,提高自适应能力

• 允许程序创建和控制任何类的对象,无需提前硬编码目标类

缺点:

• 反射的性能较低。

– 反射机制主要应用在对灵活性和扩展性要求很高的系统框架上

• 反射会模糊程序内部逻辑,可读性较差。

理解 Class 类并获取 Class 实例 

要想解剖一个类,必须先要获取到该类的 Class 对象。而剖析一个类或用反射解 决具体的问题就是使用相关 API:

java.lang.Class

java.lang.reflect.*

所以,Class 对象是反射的根源。

 理解 Class

在 Object 类中定义了以下的方法,此方法将被所有子类继承:

public final Class getClass()

以上的方法返回值的类型是一个 Class 类,此类是 Java 反射的源头,实际上所 谓反射从程序的运行结果来看也很好理解,即:可以通过对象反射求出类的名 称。

对象照镜子后可以得到的信息:某个类的属性、方法和构造器、某个类到底实 现了哪些接口。对于每个类而言,JRE 都为其保留一个不变的 Class 类型的对 象。一个 Class 对象包含了特定某个结构 (class/interface/enum/annotation/primitive type/void/[])的有关信息。

• Class 本身也是一个类

• Class 对象只能由系统建立对象

• 一个加载的类在 JVM 中只会有一个 Class 实例

• 一个 Class 对象对应的是一个加载到 JVM 中的一个.class 文件

• 每个类的实例都会记得自己是由哪个 Class 实例所生成

• 通过 Class 可以完整地得到一个类中的所有被加载的结构

• Class 类是 Reflection 的根源,针对任何你想动态加载、运行的类,唯有先获得相应的 Class 对象 

 

获取 Class 类的实例(四种方法) 

Class clazz = String.class;  //方式一
Class clazz = "www.atguigu.com".getClass();//方式二
Class clazz = Class.forName("java.lang.String");//方式三


ClassLoader cl = this.getClass().getClassLoader(); //方式四
Class clazz4 = cl.loadClass("类的全类名");

方式 1:要求编译期间已知类型

前提:若已知具体的类,通过类的 class 属性获取,该方法最为安全可靠,程序 性能最高

方式 2:获取对象的运行时类型

前提:已知某个类的实例,调用该实例的 getClass()方法获取 Class 对象

方式 3:可以获取编译期间未知的类型

前提:已知一个类的全类名,且该类在类路径下,可通过 Class 类的静态方法 forName()获取,可能抛出 ClassNotFoundException

方式 4:其他方式(不做要求)

前提:可以用系统类加载对象或自定义加载器对象加载指定路径下的类型

哪些类型可以有 Class 对象

简言之,所有 Java 类型!

.(1)class:外部类,成员(成员内部类,静态内部类),局部内部类,匿名内部 类 (2)interface:接口 (3)[]:数组 (4)enum:枚举 (5)annotation: 注解@interface (6)primitive type:基本数据类型 (7)void 

 @Test
    public void test01(){
        Class c1 = Object.class;
        Class c2 = Comparable.class;
        Class c3 = String[].class;
        Class c4 = int[][].class;
        Class c5 = ElementType.class;
        Class c6 = Override.class;
        Class c7 = int.class;
        Class c8 = void.class;
        Class c9 = Class.class;
        int[] a = new int[10];
        int[] b = new int[100];
        Class c10 = a.getClass();
        Class c11 = b.getClass();
        System.out.println(c1);
        System.out.println(c2);
        System.out.println(c3);
        System.out.println(c4);
        System.out.println(c5);
        System.out.println(c6);
        System.out.println(c7);
        System.out.println(c8);
        System.out.println(c9);
        System.out.println(c10);
        System.out.println(c11);
// 只要元素类型与维度一样,就是同一个 Class
        System.out.println(c10 == c11);
    }

 

Class 类的常用方法

static Class forName(String name) 返回指定类名 name 的 Class 对象

Object newInstance() 调用缺省构造函数,返回该 Class 对象的 一个实例

getName()返回此 Class 对象所表示的实体(类、接 口、数组类、基本类型或 void)名称

Class [] getInterfaces()         获取当前 Class 对象的接口

ClassLoader getClassLoader() 返回该类的类加载器

Field[] getDeclaredFields() 返回 Field 对象的一个数组

Method getMethod(String name,Class … paramTypes) 返回一个 Method 对象,此对象的形参类 型为 paramType

使用 ClassLoader 获取流

InputStream in = null;
in = this.getClass().getClassLoader().getResourceAsStream("exer2\\te
st.properties");
System.out.println(in);


//需要掌握如下的代码
 @Test
 public void test5() throws IOException {
 Properties pros = new Properties();
 //方式 1:此时默认的相对路径是当前的 module
// FileInputStream is = new FileInputStream("info.properties");
// FileInputStream is = new FileInputStream("src//info1.properties");
 //方式 2:使用类的加载器
 //此时默认的相对路径是当前 module 的 src 目录
 InputStream is = ClassLoader.getSystemClassLoader().getResour
ceAsStream("info1.properties");
 pros.load(is);
 //获取配置文件中的信息
 String name = pros.getProperty("name");
 String password = pros.getProperty("password");
 System.out.println("name = " + name + ", password = " + passwo
rd);
 }

 反射的基本应用

应用 1:创建运行时类的对象

这是反射机制应用最多的地方。创建运行时类的对象有两种方式:

方式 1:直接调用 Class 对象的 newInstance()方法

要 求: 1)类必须有一个无参数的构造器。2)类的构造器的访问权限需要足 够。

方式 2:通过获取构造器对象来进行实例化

方式一的步骤:

1)获取该类型的 Class 对象 2)调用 Class 对象的 newInstance()方法创建对象 

 

@Test
 public void test1() throws Exception{
// personobj = new person();//编译期间无法创建
 Class<?> clazz = Class.forName("com.ext.demo.person");
 //clazz 代表 com.ext.demo.person类型
 //clazz.newInstance()创建的就是 person的对象
 Object obj = clazz.newInstance();
 System.out.println(obj);
 }

方式二的步骤:

1)通过 Class 类的 getDeclaredConstructor(Class … parameterTypes)取得本类的 指定形参类型的构造器 2)向构造器的形参中传递一个对象数组进去,里面包 含了构造器中所需的各个参数。 3)通过 Constructor 实例化对象。 如果构造器的权限修饰符修饰的范围不可见,也可以调用

setAccessible(true)

@Test
 public void test3()throws Exception{
 //(1)获取 Class 对象
 Class<?> clazz = Class.forName("com.ext.demo.person");
 /*
 * 获取 person类型中的有参构造
 * 如果构造器有多个,我们通常是根据形参【类型】列表来获取指定的一个
构造器的
 * 例如:public demo(String title, int num)
 */
 //(2)获取构造器对象
 Constructor<?> constructor = clazz.getDeclaredConstructor(Str
ing.class,int.class);
 //(3)创建实例对象
 // T newInstance(Object... initargs) 这个 Object...是在创建对象时,给有参构造的实参列表
 Object obj = constructor.newInstance("你好",200);
 System.out.println(obj);
 }
}

 应用 2:获取运行时类的完整结构

可以获取:包、修饰符、类型名、父类(包括泛型父类)、父接口(包括泛型 父接口)、成员(属性、构造器、方法)、注解(类上的、方法上的、属性上 的)。

相关 API

//1.实现的全部接口
public Class<?>[] getInterfaces() 
//确定此对象所表示的类或接口实现的接口。
//2.所继承的父类
public Class<? Super T> getSuperclass()
//返回表示此 Class 所表示的实体(类、接口、基本类型)的父类的 Class。
//3.全部的构造器
public Constructor<T>[] getConstructors()
//返回此 Class 对象所表示的类的所有 public 构造方法。
public Constructor<T>[] getDeclaredConstructors()
//返回此 Class 对象表示的类声明的所有构造方法。
//Constructor 类中:
//取得修饰符: 
public int getModifiers();
//取得方法名称: 
public String getName();
//取得参数的类型:
public Class<?>[] getParameterTypes();
//4.全部的方法
public Method[] getDeclaredMethods()
//返回此 Class 对象所表示的类或接口的全部方法
public Method[] getMethods() 
//返回此 Class 对象所表示的类或接口的 public 的方法
//Method 类中:
public Class<?> getReturnType()
//取得全部的返回值
public Class<?>[] getParameterTypes()
//取得全部的参数
public int getModifiers()
//取得修饰符
public Class<?>[] getExceptionTypes()
//取得异常信息
//5.全部的 Field
public Field[] getFields()
//返回此 Class 对象所表示的类或接口的 public 的 Field。
public Field[] getDeclaredFields()
//返回此 Class 对象所表示的类或接口的全部 Field。
//Field 方法中:
public int getModifiers()
//以整数形式返回此 Field 的修饰符
public Class<?> getType() 
//得到 Field 的属性类型
public String getName() 
//返回 Field 的名称。
//6. Annotation 相关
get Annotation(Class<T> annotationClass)
getDeclaredAnnotations()
//7.泛型相关
//获取父类泛型类型:
Type getGenericSuperclass()
//泛型类型:ParameterizedType
//获取实际的泛型类型参数数组:
getActualTypeArguments()
//8.类所在的包
Package getPackage()

 获取所有的属性及相关细节

@Test
public void test1(){
Class clazz = Person.class;
//getFields():获取到运行时类本身及其所有的父类中声明为 public 权限
的属性
// Field[] fields = clazz.getFields();
//
// for(Field f : fields){
// System.out.println(f);
// }
//getDeclaredFields():获取当前运行时类中声明的所有属性
Field[] declaredFields = clazz.getDeclaredFields();
for(Field f : declaredFields){
System.out.println(f);
}
}
//权限修饰符 变量类型 变量名
@Test
public void test2(){
Class clazz = Person.class;
 Field[] declaredFields = clazz.getDeclaredFields();
 for(Field f : declaredFields){
 //1.权限修饰符
 /*
 * 0x 是十六进制
 * PUBLIC = 0x00000001; 1 1
 * PRIVATE = 0x00000002; 2 10
 * PROTECTED = 0x00000004; 4 100
 * STATIC = 0x00000008; 8 1000
 * FINAL = 0x00000010; 1610000
 * ...
 *
 * 设计的理念,就是用二进制的某一位是 1,来代表一种修饰符,整个二
进制中只有一位是 1,其余都是 0
 *
 * mod = 17 0x00000011
 * if ((mod & PUBLIC) != 0) 说明修饰符中有 public
 * if ((mod & FINAL) != 0) 说明修饰符中有 final
 */
 int modifier = f.getModifiers();
 System.out.print(Modifier.toString(modifier) + "\t");
// //2.数据类型
 Class type = f.getType();
 System.out.print(type.getName() + "\t");
//
// //3.变量名
 String fName = f.getName();
 System.out.print(fName);
//
 System.out.println();
 }
}
}

获取所有的方法及相关细节

@Test
public void test1() {
Class clazz = Person.class;
// getMethods():获取到运行时类本身及其所有的父类中声明为 public 权
限的方法
// Method[] methods = clazz.getMethods();
//
// for(Method m : methods){
// System.out.println(m);
// }
// getDeclaredMethods():获取当前运行时类中声明的所有方法
Method[] declaredMethods = clazz.getDeclaredMethods();
for (Method m : declaredMethods) {
System.out.println(m);
}
//
}
// 注解信息
// 权限修饰符 返回值类型 方法名(形参类型 1 参数 1,形参类型 2 参数 2,...)
throws 异常类型 1,...{}
@Test
public void test2() {
Class clazz = Person.class;
Method[] declaredMethods = clazz.getDeclaredMethods();
for (Method m : declaredMethods) {
// 1.获取方法声明的注解
Annotation[] annos = m.getAnnotations();
for (Annotation a : annos) {
System.out.println(a);
}
// 2.权限修饰符
System.out.print(Modifier.toString(m.getModifiers()) + "\t
");
// 3.返回值类型
System.out.print(m.getReturnType().getName() + "\t");
// 4.方法名
System.out.print(m.getName());
System.out.print("(");
// 5.形参列表
Class[] parameterTypes = m.getParameterTypes();
if (!(parameterTypes == null && parameterTypes.length ==
0)) {
for (int i = 0; i < parameterTypes.length; i++) {
if (i == parameterTypes.length - 1) {
System.out.print(parameterTypes[i].getName() + "
args_" + i);
break;
}
System.out.print(parameterTypes[i].getName() + " ar
gs_" + i + ",");
}
}
System.out.print(")");
// 6.抛出的异常
Class[] exceptionTypes = m.getExceptionTypes();
if (exceptionTypes.length > 0) {
System.out.print("throws ");
for (int i = 0; i < exceptionTypes.length; i++) {
if (i == exceptionTypes.length - 1) {
System.out.print(exceptionTypes[i].getName());
break;
}
System.out.print(exceptionTypes[i].getName() + ",
");
}
}
System.out.println();
}
}
}

获取其他结构(构造器、父类、接口、包、注解等)

/*
 获取当前类中的所有的构造器
 */
 @Test
 public void test1(){
 Class clazz = Person.class;
 Constructor[] cons = clazz.getDeclaredConstructors();
 for(Constructor c :cons){
 System.out.println(c);
 }
 }
 /*
 获取运行时类的父类
 */
 @Test
 public void test2(){
 Class clazz = Person.class;
 Class superclass = clazz.getSuperclass();
 System.out.println(superclass);//class com.atguigu.java1.Crea
ture
 }
 /*
 获取运行时类的所在的包
 */
 @Test
 public void test3(){
 Class clazz = Person.class;
 Package pack = clazz.getPackage();
 System.out.println(pack);
 }
 /*
 获取运行时类的注解
 */
 @Test
 public void test4(){
 Class clazz = Person.class;
 Annotation[] annos = clazz.getAnnotations();
 for (Annotation anno : annos) {
 System.out.println(anno);
 }
 }
 /*
 获取运行时类所实现的接口
 */
 @Test
 public void test5(){
 Class clazz = Person.class;
 Class[] interfaces = clazz.getInterfaces();
 for (Class anInterface : interfaces) {
 System.out.println(anInterface);
 }
 }
 /*
 获取运行时类的带泛型的父类
 */
 @Test
 public void test6(){
 Class clazz = Person.class;
 Type genericSuperclass = clazz.getGenericSuperclass();
 System.out.println(genericSuperclass);//com.atguigu.java1.Cre
ature<java.lang.String>
 }

应用 3:调用运行时类的指定结构

在反射机制中,可以直接通过 Field 类操作类中的属性,通过 Field 类提供的 set()和 get()方法就可以完成设置和取得属性内容的操作。

(1)获取该类型的 Class 对象

Class clazz = Class.forName("包.类名");

(2)获取属性对象

Field field = clazz.getDeclaredField("属性名")

(3)如果属性的权限修饰符不是 public,那么需要设置属性可访问

field.setAccessible(true);

(4)创建实例对象:如果操作的是非静态属性,需要创建实例对象

Object obj = clazz.newInstance(); //有公共的无参构造

Object obj = 构造器对象.newInstance(实参...);//通过特定构造器对象创建实例 对象

(4)设置指定对象 obj 上此 Field 的属性内容

field.set(obj,"属性值");

如果操作静态变量,那么实例对象可以省略,用 null 表示

(5)取得指定对象 obj 上此 Field 的属性内容

Object value = field.get(obj);

如果操作静态变量,那么实例对象可以省略,用 null 表示

public class Student {
 private int id;
 private String name;
 public int getId() {
 return id;
 }
 public void setId(int id) {
 this.id = id;
 }
 public String getName() {
 return name;
 }
 public void setName(String name) {
 this.name = name;
 }
 @Override
 public String toString() {
 return "Student{" +
 "id=" + id +
 ", name='" + name + '\'' +
 '}';
 }
 public static void main(String[] args)throws Exception {
 //1、获取 Student 的 Class 对象
 Class clazz = Class.forName("com.reflect.Student");
 //2、获取属性对象,例如:id 属性
 Field idField = clazz.getDeclaredField("id");
 //3、如果 id 是私有的等在当前类中不可访问 access 的,我们需要做如下
操作
 idField.setAccessible(true);
 //4、创建实例对象,即,创建 Student 对象
 Object stu = clazz.newInstance();
 //5、获取属性值
 /*
 * 以前:int 变量= 学生对象.getId()
 * 现在:Object id 属性对象.get(学生对象)
 */
 Object value = idField.get(stu);
 System.out.println("id = "+ value);
 //6、设置属性值
 /*
 * 以前:学生对象.setId(值)
 * 现在:id 属性对象.set(学生对象,值)
 */
 idField.set(stu, 2);
 value = idField.get(stu);
 System.out.println("id = "+ value);
 }

关于 setAccessible 方法的使用:

• Method 和 Field、Constructor 对象都有 setAccessible()方法。

• setAccessible 启动和禁用访问安全检查的开关。

• 参数值为 true 则指示反射的对象在使用时应该取消 Java 语言访问检查。

– 提高反射的效率。如果代码中必须用反射,而该句代码需要频繁的被调 用,那么请设置为 true。

– 使得原本无法访问的私有成员也可以访问

• 参数值为 false 则指示反射的对象应该实施 Java 语言访问检查。

调用指定的方法 

 

(1)获取该类型的 Class 对象

Class clazz = Class.forName("包.类名")

(2)获取方法对象 

Method method = clazz.getDeclaredMethod("方法名",方法的形参类型列表);

(3)创建实例对象

Object obj = clazz.newInstance();

(4)调用方法

Object result = method.invoke(obj, 方法的实参值列表);

如果方法的权限修饰符修饰的范围不可见,也可以调用

setAccessible(true)

如果方法是静态方法,实例对象也可以省略,用 null 代替

@Test
 public void test()throws Exception {
 // 1、获取 Student 的 Class 对象
 Class<?> clazz = Class.forName("com.reflect.Student
");
 //2、获取方法对象
 /*
 * 在一个类中,唯一定位到一个方法,需要:(1)方法名(2)形参列表,
因为方法可能重载
 *
 * 例如:void setName(String name)
 */
 Method setNameMethod = clazz.getDeclaredMethod("setName", Str
ing.class);
 //3、创建实例对象
 Object stu = clazz.newInstance();
 //4、调用方法
 /*
 * 以前:学生对象.setName(值)
 * 现在:方法对象.invoke(学生对象,值)
 */
 Object setNameMethodReturnValue = setNameMethod.invoke(stu, "
张三");
 System.out.println("stu = " + stu);
 //setName 方法返回值类型 void,没有返回值,所以 setNameMethodRetu
rnValue 为 null
 System.out.println("setNameMethodReturnValue = " + setNameMet
hodReturnValue);
 Method getNameMethod = clazz.getDeclaredMethod("getName");
 Object getNameMethodReturnValue = getNameMethod.invoke(stu);
 //getName 方法返回值类型 String,有返回值,getNameMethod.invoke
的返回值就是 getName 方法的返回值
 System.out.println("getNameMethodReturnValue = " + getNameMet
hodReturnValue);//张三
 }
 @Test
 public void test02()throws Exception{
 Class<?> clazz = Class.forName("com.ext.demo.student");
 Method printInfoMethod = clazz.getMethod("printInfo", String.
class);
 //printInfo 方法是静态方法
 printInfoMethod.invoke(null,"hello");
 }

 一个完整的注解应该包含三个部分: (1)声明 (2)使用 (3)读取

体会反射的动态性

体会 1:

public class ReflectionTest {
 //体会反射的动态性:动态的创建给定字符串对应的类的对象
 public <T> T getInstance(String className) throws Exception {
 Class clazz = Class.forName(className);
 Constructor constructor = clazz.getDeclaredConstructor();
 constructor.setAccessible(true);
 return (T) constructor.newInstance();
 }
 @Test
 public void test1() throws Exception {
 String className = "com.java1.Person";
 Person p1 = getInstance(className);
 System.out.println(p1);
 }
}

体会 2:

public class ReflectionTest {
 //体会反射的动态性:动态的创建指定字符串对应类的对象,并调用指定的方法
 public Object invoke(String className,String methodName) throws
Exception {
 Class clazz = Class.forName(className);
 Constructor constructor = clazz.getDeclaredConstructor();
 constructor.setAccessible(true);
 //动态的创建指定字符串对应类的对象
 Object obj = constructor.newInstance();
 Method method = clazz.getDeclaredMethod(methodName);
 method.setAccessible(true);
 return method.invoke(obj);
 }
 @Test
 public void test2() throws Exception {
 String info = (String) invoke("com.java1.Person", "sh
ow");
 System.out.println("返回值为:" + info);
 }
}

体会 3:

public class ReflectionTest {
@Test
 public void test1() throws Exception {
 //1.加载配置文件,并获取指定的 fruitName 值
 Properties pros = new Properties();
 InputStream is = ClassLoader.getSystemClassLoader().getResour
ceAsStream("config.properties");
 pros.load(is);
 String fruitStr = pros.getProperty("fruitName");
 //2.创建指定全类名对应类的实例
 Class clazz = Class.forName(fruitStr);
 Constructor constructor = clazz.getDeclaredConstructor();
 constructor.setAccessible(true);
 Fruit fruit = (Fruit) constructor.newInstance();
 //3. 调用相关方法,进行测试
 Juicer juicer = new Juicer();
 juicer.run(fruit);
 }
}
interface Fruit {
public void squeeze();
}
class Apple implements Fruit {
public void squeeze() {
System.out.println("榨出一杯苹果汁儿");
}
}
class Orange implements Fruit {
public void squeeze() {
System.out.println("榨出一杯桔子汁儿");
}
}
class Juicer {
public void run(Fruit f) {
f.squeeze();
}
}

其中,配置文件【config.properties】存放在当前 Module 的 src 下

lambda 表达式

语法格式一:无参,无返回值

@Test
public void test1(){
 //未使用 Lambda 表达式
 Runnable r1 = new Runnable() {
 @Override
 public void run() {
 System.out.println("我爱北京天安门");
 }
 };
 r1.run();
 System.out.println("***********************");
 //使用 Lambda 表达式
 Runnable r2 = () -> {
 System.out.println("我爱北京故宫");
 };
 r2.run();
}

语法格式二:Lambda 需要一个参数,但是没有返回值。

@Test
public void test2(){
 //未使用 Lambda 表达式
 Consumer<String> con = new Consumer<String>() {
 @Override
 public void accept(String s) {
 System.out.println(s);
 }
 };
 con.accept("谎言和誓言的区别是什么?");
 System.out.println("*******************");
 //使用 Lambda 表达式
 Consumer<String> con1 = (String s) -> {
 System.out.println(s);
 };
 con1.accept("一个是听得人当真了,一个是说的人当真了");
}

语法格式三:数据类型可以省略,因为可由编译器推断得出,称为“类型推断”

@Test
public void test3(){
 //语法格式三使用前
 Consumer<String> con1 = (String s) -> {
 System.out.println(s);
 };
 con1.accept("一个是听得人当真了,一个是说的人当真了");
 System.out.println("*******************");
 //语法格式三使用后
 Consumer<String> con2 = (s) -> {
 System.out.println(s);
 };
 con2.accept("一个是听得人当真了,一个是说的人当真了");
}

语法格式四:Lambda 若只需要一个参数时,参数的小括号可以省略

@Test
public void test4(){
 //语法格式四使用前
 Consumer<String> con1 = (s) -> {
 System.out.println(s);
 };
 con1.accept("一个是听得人当真了,一个是说的人当真了");
 System.out.println("*******************");
 //语法格式四使用后
 Consumer<String> con2 = s -> {
 System.out.println(s);
 };
 con2.accept("一个是听得人当真了,一个是说的人当真了");
}

语法格式五:Lambda 需要两个或以上的参数,多条执行语句,并且可以有返 回值

@Test
public void test5(){
 //语法格式五使用前
 Comparator<Integer> com1 = new Comparator<Integer>() {
 @Override
 public int compare(Integer o1, Integer o2) {
 System.out.println(o1);
 System.out.println(o2);
 return o1.compareTo(o2);
 }
 };
 System.out.println(com1.compare(12,21));
 System.out.println("*****************************");
 //语法格式五使用后
 Comparator<Integer> com2 = (o1,o2) -> {
 System.out.println(o1);
 System.out.println(o2);
 return o1.compareTo(o2);
 };
 System.out.println(com2.compare(12,6));
}

语法格式六:当 Lambda 体只有一条语句时,return 与大括号若有,都可以省 略

@Test
public void test6(){
 //语法格式六使用前
 Comparator<Integer> com1 = (o1,o2) -> {
 return o1.compareTo(o2);
 };
 System.out.println(com1.compare(12,6));
 System.out.println("*****************************");
 //语法格式六使用后
 Comparator<Integer> com2 = (o1,o2) -> o1.compareTo(o2);
 System.out.println(com2.compare(12,21));
}
@Test
public void test7(){
 //语法格式六使用前
 Consumer<String> con1 = s -> {
 System.out.println(s);
 };
 con1.accept("一个是听得人当真了,一个是说的人当真了");
 System.out.println("*****************************");
 //语法格式六使用后
 Consumer<String> con2 = s -> System.out.println(s);
 con2.accept("一个是听得人当真了,一个是说的人当真了");
}

  • 22
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值