Java SE 04(Object类、常用API、集合与泛型、异常、线程、等待唤醒机制、Lambda表达式)

Java SE 04

目录

一、Object类、常用API

Object类的toString方法

  • Java.lang.Object类是类层次结构的根类(最顶层)。每个类都使用了Object类作为父类(超类)。所有对象(包括数组)都实现了这个类的方法。

  • 所有的类都继承了Object类,所以可以使用Object类中的toString方法

  • 每次使用System.out.println()直接打印对象都会调用一次toString方法,String类也是继承了Object类但重写了toString方法,所以打印出的String类的对象是重写(override)方法中格式调整好的字符串

  • 自定义一个类,其中是默认继承了Object类中toString方法,若未重写,按照原方法的格式(包名类名@地址值)打印地址值

  • 直接打印对象的地址不常见,一般写类的时候要重写Object类的toString方法,也就是说在定义类的方法时除了写getXxx/setXxx方法,还要重写toString方法(idea快捷键Alt+Insert),这样打印出的对象值可以看到对象中的属性值

    //toString方法输出格式:类名{成员变量1=xxx, 成员变量2=xxx}
    //Person{age=45, name='Jeff'}
    public class Person {
        private int age;
        private String name;
    
        @Override
        public String toString() {
            return "Person{" +
                    "age=" + age +
                    ", name='" + name + '\'' +
                    '}';
        }
        //...
    }
    
  • 判断一个类是否重写了toString方法,直接打印这个类的对象。

    若没有重写toString方法,打印的是这个对象的地址值(默认)

    如果重写了toString方法,那么按照重写的方式打印

    public class Demo01 {
        public static void main(String[] args) {
            Scanner scanner = new Scanner(System.in);
            System.out.println(scanner.toString());//Scanner中重写了toString方法
    
            Random random = new Random();
            System.out.println(random.toString());//Random中未重写toString方法
    
            ArrayList<Integer> list = new ArrayList<>();
            System.out.println(list.toString());//Arraylist<Integer>中重写了toString方法
        }
    }
    /*
    output:
    java.util.Scanner[delimiters=\p{javaWhitespace}+][position=0][match valid=false][need input=false][source closed=false][skipped=false][group separator=\x{2c}][decimal separator=\x{2e}][positive prefix=][negative prefix=\Q-\E][positive suffix=][negative suffix=][NaN string=\QNaN\E][infinity string=\Q∞\E]
    
    java.util.Random@61dc03ce
    
    []
    */
    

Object类的equals方法

  • 所有的类都默认继承了Object类,所以可以使用Objcet类的equals方法

  • boolean equals(Object obj)指示其他某个对象是否与此对象“相等”

  • Object类中的equals方法的源码:

    public boolean equals(Object obj) {
        return (this == obj);
    }
    

    参数:Object obj:可以传递任意的对象

    方法体:

    ==比较运算符,返回的就是一个布尔值true,false

    基本数据类型:比较的是值

    引用数据类型:比较的是两个对象的地址值

    关于this:哪个对象调用了方法,方法中的this就是那个对象。p1调用了equals方法,所以方法中的this就是p1这个对象

  • Object类的equals方法默认是比较两个对象的地址值,如果比较两个对象的成员变量值(属性值)是否相同,一般要重写equals方法,比较两个对象的属性(成员变量)值

    重写equals方法(idea快捷键Alt+Insert)

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;//使用了反射,判断o是否为Person类型,等效于instanceof
        Person person = (Person) o;
        return age == person.age && name.equals(person.name);
    }
    
    //另一种写法
    @Override
    public boolean equals(Object object) {
    
        if(object == this) {
            return true;
        }
    
        if(object == null) {
            return false;
        }
    
        if(object instanceof Person){
            //Object object = new Person();
            Person person = (Person)object;//类型高转低,前提条件是先要用instanceof判断object是否是Person类new创建的,所以传参如果是Person类型的对象,就能通过该instanceof检测
            return name.equals(person.name) && age == person.age;
        }
        return false;
    }
    

Objects类的equals方法

  • JDK7中添加了Objects工具类,提供了一些方法来操作对象,它由一些静态的实用方法组成,这些方法是null-save(空指针安全的)或是null-tolerant(空指针容忍的)

  • Object类中equals使用空指针调用对象会报错,Objects类中的equals可以使用两个空指针进行比较

    //System.out.println(null.equals(null));//Error
    System.out.println(Objects.equals(null, null));//output: True
    
  • Objects类的equals方法是个静态方法,可以直接通过类名.成员方法直接使用

  • Objects类的equals方法:对两个对象进行比较,可以防止空指针异常

    //Objects的equals方法实现
    public static boolean equals(Object a, Object b) {
        return (a == b) || (a != null && a.equals(b));//加了健壮性判断
    }
    

Date类

  • java.util.Date是一个表示日期和时间的类,精确到毫秒(1s = 1000ms)

  • 对时间和日期进行计算时,可以将日期转换为毫秒进行计算,计算完毕再将毫秒转为日期,

    规定的时间原点(0毫秒):1970年1月1日00:00:00(英国格林威治时间),中国属于东八区,会把时间增加8个小时,就是计算当前日期到时间原点之间一共经历了多少毫秒(使用System.currentTimeMillis()的返回值)

  • Date类的无参构造函数

    Date()就是获取当前系统的日期和时间

    public class Demo02 {
        public static void main(String[] args) {
            System.out.println(new Date());//Mon May 17 14:51:54 CST 2021
        }
    }
    
  • Date类的带参构造函数

    Date(long date):传递毫秒值,把毫秒转换为Date日期

  • Date类的成员方法

    long getTime()将当前日期转换为毫秒值,getTime()相当于使用System.currentTimeMillis()

    public class Demo02 {
        public static void main(String[] args) {
            System.out.println(new Date().getTime());
            System.out.println(System.currentTimeMillis());
        }
    }
    /*
    output:
    1621236536108
    1621236536108
    */
    
  • DateFormat类

    java.text.DateFormat是日期/时间格式化子类的抽象类(public abstract class DateFormat extends Format),作用是可以完成日期和文本之间的转换,也就是可以在Date对象与String对象之间进行来回转换

    • 格式化:按指定的格式,使用String format(Date date)成员方法从Date对象转换为String对象

      import java.text.SimpleDateFormat;
      import java.util.Date;
      
      public class Demo03 {
          public static void main(String[] args) {
              Date date = new Date();
              System.out.println(date);
      
              SimpleDateFormat dateFormat1 = new SimpleDateFormat();
              SimpleDateFormat dateFormat2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
              SimpleDateFormat dateFormat3 = new SimpleDateFormat("yyyy**MM**dd HH**mm**ss");
      
              String dateStr1 = dateFormat1.format(date);//SimpleDateFormat是DateFormat的子类,所以继承了format方法,Format是抽象类只能用SimpleDateFormat实现
              System.out.println(dateStr1);
              String dateStr2 = dateFormat2.format(date);
              System.out.println(dateStr2);
              String dateStr3 = dateFormat3.format(date);
              System.out.println(dateStr3);
          }
      }
      /*
      output:
      Mon May 17 17:27:51 CST 2021
      5/17/21, 5:27 PM
      2021-05-17 17:27:51
      2021**05**17 17**27**51
      */
      
    • 解析:按指定的格式,使用Date parse(String string)从String对象转换为Date对象

      import java.text.ParseException;
      import java.text.SimpleDateFormat;
      import java.util.Date;
      
      public class Demo04 {
          public static void main(String[] args) throws ParseException {
              SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
              Date date = dateFormat.parse("2021-05-19 16:35:23");
              System.out.println(date);
          }
      }
      /*
      Wed May 19 16:35:23 CST 2021
      */
      
    • DateFormat类是一个抽象类,无法直接创建对象使用,可以使用DateFormat子类,例如SimpleDateFormat

    • java.text.SimpleDateFormat extends DateFormat

      构造方法:SimpleDateFormat(String pattern)用给定的模式和默认语言环境的日期格式符号构造SimpleDateFormat

      模式字符串区分大小写:y年 M月 d日 H时 m分 s秒

      写对应的模式,会把模式替换为对应的日期和时间

      “yyyy-MM-dd HH:mm:ss”

      "yyyy年MM月dd日 HH时mm分ss秒"

      注意:

      模式中的字母不能更改,连接模式的符号可以更改

Calendar类介绍

  • java.util.Calendar类是一个日历类

  • Calendar类是一个抽象类,里边提供了很多操作日历字段的方法(YEAR、MONTH、DAY_OF_MONTH、HOUR)

  • Calendar类无法直接创建对象使用,里面有一个静态方法叫getInstance()该方法返回了Calendar类的子类对象

  • static Calendar getInstance()使用了默认时区和语言环境获得一个日历

    public class Demo06 {
        public static void main(String[] args) {
            Calendar calendar = Calendar.getInstance()//多态
            System.out.println(calendar);
        }
    }
    

Calendar类常用方法

  • 成员方法的参数:int field:日历类的字段,可以使用Calendar类的静态成员变量获取

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

    参数:传递指定的日历字段(YEAR, MONTH...)

    注意:MONTH字段对应的值是从0-11,使用时手动+1

    返回值:日历字段代表具体的值

  • void set(int field, int value);

    设置字段的值

  • abstract void add(int field, int value);

    改变字段的值,value为正则增加,value为负则减少

  • Date getTime()

    将Calendar的对象转为Date类型的对象

    打印出是日期值

import java.util.Calendar;
import java.util.Date;

public class Demo06 {
    public static void main(String[] args) {
        Calendar calendar = Calendar.getInstance();
        System.out.println(calendar);

        int year = calendar.get(Calendar.YEAR);
        System.out.println(year);

        calendar.set(Calendar.YEAR, 2019);
        year = calendar.get(Calendar.YEAR);
        System.out.println(year);

        calendar.add(Calendar.YEAR, -2);
        year = calendar.get(Calendar.YEAR);
        System.out.println(year);

        Date date = calendar.getTime();
        System.out.println(date);
    }
}

/*
java.util.GregorianCalendar[time=1621266704832,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="Asia/Shanghai",offset=28800000,dstSavings=0,useDaylight=false,transitions=31,lastRule=null],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2021,MONTH=4,WEEK_OF_YEAR=21,WEEK_OF_MONTH=4,DAY_OF_MONTH=17,DAY_OF_YEAR=137,DAY_OF_WEEK=2,DAY_OF_WEEK_IN_MONTH=3,AM_PM=1,HOUR=11,HOUR_OF_DAY=23,MINUTE=51,SECOND=44,MILLISECOND=832,ZONE_OFFSET=28800000,DST_OFFSET=0]
2021
2019
2017
Wed May 17 23:51:44 CST 2017

*/

System类

  • java.lang.System类中提供了大量的静态方法,可以获取与系统相关的信息或系统级操作,在System类的API文档中,常用的方法有

    • public static long currentTimeMillis()

      返回以毫秒为单位的当前时间

      //用于计算一段代码的运行时间
      public class Demo08 {
          public static void main(String[] args) {
              long cntTime01 = System.currentTimeMillis();
              for(int i = 0; i < 10000; i++) {
                  System.out.println("Hello");
              }
              long cntTime02 = System.currentTimeMillis();
      
              double runTime = (cntTime02 - cntTime01) / (double)1000;
              System.out.println("running time is " + runTime);
          }
      }
      
    • public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)

      将数组中指定的数据拷贝到另一个数组中

      //目的数组中拷贝到的位置若有内容会被源数组的内容覆盖
      public class Demo07 {
          public static void main(String[] args) {
              int[] dstArr = new int[20];
              dstArr[0] = 11;
              dstArr[1] = 12;
              dstArr[2] = 13;
              dstArr[5] = 14;
              int[] srcArr = new int[10];
              srcArr[0] = 1;
              srcArr[1] = 2;
              srcArr[2] = 3;
              srcArr[3] = 4;
              srcArr[4] = 5;
              srcArr[5] = 6;
      
              System.out.println(Arrays.toString(srcArr));
      
              //public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)
      
              System.arraycopy(srcArr, 0, dstArr, 0, 5);
      
              System.out.println(Arrays.toString(dstArr));
          }
      }
      /*
      [1, 2, 3, 4, 5, 6, 0, 0, 0, 0]
      [1, 2, 3, 4, 5, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
      */
      

StringBuilder类

  • String类中字符串是常量,他们的值在创建之后不能再修改。字符串的底层是一个被final修饰的数组,是一个常量。private final byte[] value;

  • 使用String类进行字符串相加,内存中就会有多个字符串,占空间多,效率低下

    String s = "a" + "b" + "c" ;这个字符串相加操作内存中本身产生3个字符串,还会产生"ab", “abc”,一共要产生5个字符串

  • StringBuilder类是字符串缓冲区,可以提高字符串操作的效率,可以看成是长度可变的字符串

    底层也是数组,但没有被final修饰,byte[] value = new byte[16];(数组初始容量是16)

  • StringBuilder在内存中始终是一个数组,占用空间少,效率高。

    如果超出了StringBuilder的容量,会自动扩容

StringBuilder的构造方法

StringBuilder的常用构造方法有2个:

  • public StringBuilder():构造一个空的StringBuilder容器
  • public StringBuilder(String str):构造一个StringBuilder容器,并将字符串添加进去

StringBuilder的常用方法

StringBuilder的常用方法有2个:

  • public StringBuilder append(...):添加任意类型数据的字符串形式,并返回当前对象自身。

    因为append方法返回的也是StringBuilder类型的,所以可以使用链式方法调用连续添加元素.

    使用append方法无需接收返回值

    public class Demo09 {
        public static void main(String[] args) {
            StringBuilder sb = new StringBuilder();
            StringBuilder sb2 = sb.append("Hello").append(3).append("World").append('@');
            System.out.println(sb2);
            System.out.println(sb == sb2);//地址值一样,相当于append方法返回的是this
        }
    }
    
  • public StringBuilder reverse();反转StringBuilder中的元素

  • public String toString():将当前StringBuilder的对象转为String对象

    StringBuilder和String可以相互转换

    • String转StringBuilder可以使用StringBuilder类的构造方法
    • StringBuilder转String可以使用StringBuilder的toString方法

包装类

  • 基本数据类型的数据,使用十分方便,但是没有对应的方法来操作这些数据,所以我们可以使用一个类,把基本数据类型的数据包装起来,这个类叫包装类。在包装类中可以定义一些方法,用来操作基本的数据类型。

  • Java中8种基本数据类型都有对应的包装类,在java.lang包下,一般使用无需导包。

    int->Integer short->Short long->Long byte->Byte

    float->Float double->Double

    char->Character

    boolean->Boolean

包装类装箱与拆箱

  • 装箱:从基本数据类型转换为对应的包装类对象

    构造方法:

    Integer(int value):构造一个新分配的Integer对象,它表示指定的int值

    Integer(String s):构造一个新分配的Integer对象,它表示String参数所指示的int值,其中传递的字符串必须是基本类型的字符串,否则会抛异常,“100”表示正确,"a"抛异常

    静态方法:

    static Integer valueOf(int i)返回一个表示指定的int值的Integer实例

    static Integer valueOf(String s)返回保存指定的String值的Integer对象

  • 拆箱:从包装类对象转换为对应的基本类型

    成员方法:

    int intValue()以int类型返回该Integer的值

public class Demo10 {
    public static void main(String[] args) {
       Integer integer = new Integer(22);
       Integer integer1 = new Integer("3245");

       Integer integer2 = Integer.valueOf(23);
       System.out.println(integer2);
       Integer integer3 = Integer.valueOf("4590");
       System.out.println(integer3);

       int num1 = integer2.intValue();
       System.out.println(num1);
       int num2 = integer3.intValue();
       System.out.println(num2);
    }
}
/*
output:
23
4590
23
4590
*/

自动装箱和自动拆箱

  • JDK1.5之后出现的新特性,基本类型的数据和包装类之间可以自动地相互转换

  • 自动装箱:直接把int类型的整数赋值给包装类

    Integer num = 1; 就相当于Integer num = new Integer(1);

  • num是包装类的对象不能直接参与运算,但是可以自动转换为基本类型的数据,再参与计算

    num + 2; 就相当于num.intValue() + 2得到int类型的数字3

    num = num + 2;就相当于num = new Integer(3);

  • ArrayList集合无法直接存储整数,可以存储Integer包装类

    ArrayList<Integer> list = new ArrayList<>();
    list.add(1);//自动装箱, 相当于自动执行了list.add(new Integer(1));
    int num = list.get(0);//自动拆箱,相当于自动执行了list.get(0).intValue();
    

基本类型与字符串类型相互转换

  • 基本类型转为字符串

    1. 基本类型数据值+""(加一个空字符串)

      ...

      public class Demo11 {
          public static void main(String[] args) {
              int num = 1;
              String str = 1 + "" + 2;
              System.out.println(str);//output: 12
          }
      }
      
    2. 使用包装类中的静态方法

      static String toString(int i)返回指定整数的String对象

      ...

      public class Demo11 {
          public static void main(String[] args) {
              String str = Integer.toString(3);
              System.out.println(str + 2);//output: 32
          }
      }
      
    3. 使用String类中的静态方法

      static String valueOf(int i)返回int参数的字符串表示形式

      ...

      public class Demo11 {
          public static void main(String[] args) {
              String str = String.valueOf(32);
              System.out.println(str + 1);//output: 321
          }
      }
      
  • 字符串转基本类型

    1. 使用包装类的静态方法parseXx(String str)

      static int parseInt(String s)

      static double parseDouble(String s)

      public class Demo11 {
          public static void main(String[] args) {
              int num = Integer.parseInt("787");
              System.out.println(num + 2);//output: 789
          }
      }
      
    2. static Integer valueOf(String s)返回保存指定的String值的Integer对象

      int num2  = Integer.valueOf("4545");
      int num3  = Integer.parseInt("45");
      System.out.println(num2 + 2);//output: 4547
      System.out.println(num3 + 1);//output: 46
      

二、Collection集合、泛型

集合和数组的区别

  • 集合:集合是Java中提供的一种容器,可以用来存储多个数据

  • 区别:

    1. 数组的长度是固定的,而集合的长度是可变的

    2. 数组中存储的是同一类型的元素,可以存储基本的数据类型值或引用类型变量值。

      集合不能存储基本数据类型变量值,集合存储的都是引用类型变量值,而且这些变量的引用类型可以不一样。在开发中当使用多种引用类型变量时,使用集合进行存储。

集合框架

(单列集合的体系结构)

  • Collection接口(单列集合最顶层的接口)有两大子接口(extends了Collection)List和Set

    定义的是所有单列集合中共性的方法,所有的单列集合都可以使用共性的方法

    注意:Collection接口中没有带索引的方法

  • 两大接口List和Set

    List接口的特点:

    1. 有序的集合(存储和取出元素顺序相同)
    2. 允许存储重复的内容
    3. 有索引(可以使用普通的for循环遍历)
    4. List接口的实现类有Vector、ArrayList(底层是数组实现的,查询快,增删慢)、LinkedList(底层是链表实现的,查询慢,增删快)

    Set接口的特点:

    1. 不允许存储重复的元素
    2. 没有索引(不能使用普通for循环遍历)
    3. TreeSet和HashSet是无序集合(存储和取出元素的顺序有可能不一致),但LinkedHashSet是有序集合
    4. Set接口实现类有TreeSet(底层是二叉树实现。一般用于排序)、HashSet(底层是哈希表+红黑树实现),其中HashSet的子类有LinkedHashSet(底层是哈希表+链表实现)

Collection中的方法

java.util.Collection接口是所有单列集合的最顶层的接口,里面定义了所有单列集合共性的方法

所有的单列集合都可以使用Collection接口中共性的方法

  • boolean add(E e);向集合中添加元素, 成功添加返回true
  • boolean remove(E e);删除集合中的某个元素,若没有该元素则返回false
  • void clear();清空集合中的所有元素
  • boolean contains(E e);判断集合中是否包含某个元素,不包含该元素返回false
  • boolean isEmpty();判断集合是否为空,不空返回false
  • int size();获取集合的长度, 返回集合元素的个数
  • Object[] toArray();集合转成一个数组
public class Demo01 {
    public static void main(String[] args) {
        Collection<Integer> coll = new ArrayList<>();//创建集合可以使用多态
        System.out.println(coll);//output:[]		 //说明ArrayList父类的父类中重写了toString方法
    }
}

Iterator接口(迭代器)

Java.util.Iterator接口:迭代器(对集合进行遍历,因为Collection中没有带索引的方法,所以规定也不能使用索引进行遍历,只能用迭代器来实现遍历,其实也是为了兼顾一些没有索引的集合)

  • 两个常用方法

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

    E next()返回迭代的下一个元素

  • Iterator迭代器,是一个接口,无法直接使用,需要使用Iterator接口的实现类对象,获取方式比较特殊

    Collection接口中有一个方法叫iterator(), 这个方法返回的就是迭代器的实现类对象,Iterator iterator() 返回在此collection(Collection类型的对象)的元素上进行迭代的迭代器

  • 注意事项:

    迭代器的泛型要和集合一致

    Iterator接口也是有泛型的,迭代器的泛型跟着集合走,集合是什么泛型,迭代器就是什么泛型

  • 迭代器的使用步骤:

    1. 使用某个集合的对象调用集合中的方法iterator()获取迭代器的实现类对象,使用Iterator接口接收一个实现类(多态)
    2. 使用Iterator接口中的方法hasNext()判断有没有下一个元素
    3. 使用Iterator接口中的方法next()取出集合中的下一个元素
    public class Demo02 {
        public static void main(String[] args) {
            Collection<Integer> coll = new ArrayList<>();//多态
            coll.add(5);
            coll.add(6);
            coll.add(2);
            coll.add(8);
    
            //Iterator<E>接口也是有泛型的,迭代器的泛型跟着集合走,集合是什么泛型,迭代器就是什么泛型
            Iterator<Integer> iter = coll.iterator();//iterator()方法返回的的是Iterator接口的实现类对象,这里用Iterator类型的变量去接收实现类的对象是多态的现象
            while(iter.hasNext()) {
                System.out.println(iter.next());
            }
    
            System.out.println("=============");
    
            Iterator<Integer> iter2 = coll.iterator();
            for(;iter2.hasNext();) {
                System.out.println(iter2.next());
            }
        }
    }
    /*
    output:
    5
    6
    2
    8
    =============
    5
    6
    2
    8
    */
    

迭代器的实现原理

假设把ArrayList的对象想象成一个数组,它的0索引前还有-1索引,在最开始获取迭代器的实现类对象时假想有一个指针指向-1索引,当iter.hasnext()判断出有下一个元素,使用iter.next()取出下一个元素并且把当前指针向后移动一个位置

public class Demo02 {
    public static void main(String[] args) {
        Collection<Integer> coll = new ArrayList<>();//多态
        coll.add(5);
        coll.add(6);
        coll.add(2);
        coll.add(8);

        //Iterator<E>接口也是有泛型的,迭代器的泛型跟着集合走,集合是什么泛型,迭代器就是什么泛型
        Iterator<Integer> iter = coll.iterator();//获取迭代器的实现类对象,并且会把指针(索引)指向集合-1索引
        while(iter.hasNext()) {//在这里会判断还有没有下一个元素
            System.out.println(iter.next());//iter.next()作用是:1. 取出下一个元素	2. 会把指针向后移动一位
        }

        System.out.println("=============");

        Iterator<Integer> iter2 = coll.iterator();
        for(;iter2.hasNext();) {
            System.out.println(iter2.next());
        }
    }
}

增强for循环(for each)

  • 增强for循环也称for each循环是JDK1.5之后出现的一个高级for循环,专门用来遍历数组和集合它的内部原理其实是个Iterator迭代器,所以在遍历的过程中,不能对集合中的元素进行增删改查

  • Collection接口继承了Iterable接口,Collection extends Iterable

    public interface Iterable实现这个接口允许对象成为"for each"语句的目标

    所以所有的单列集合都可以使用增强for

格式:
    for(集合/数组元素的数据类型	变量名(循环每次执行时,表示遍历到的第n个元素) : 集合名/数组名) {
        System.out.println(变量名);
    }
import java.util.ArrayList;

public class Demo04 {
    public static void main(String[] args) {
        newForLoopArr();
        System.out.println("========");
        newForLoopArrList();
    }

    public static void newForLoopArr() {
        int[] arr = new int[] {1, 3, 4, 3, 2 ,1};
        for(int i : arr) {
            System.out.println(i);
        }
    }

    public static void newForLoopArrList() {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(6);
        list.add(1);
        list.add(3);
        list.add(2);
        for(int i : list) {
            System.out.println(i);
        }
    }
}
/*
output:
1
3
4
3
2
1
========
6
1
3
2
*/

泛型

  • 泛型是一种未知的数据类型,当我们不知道使用什么数据类型的时候,可以使用泛型

  • 泛型也可以看作是一种变量,用来接收数据类型

  • E是未知的数据类型,创建集合对象的时候,就会确定泛型的数据类型(会把数据类型作为参数传递)

    E e : Element元素

    T t: Type类型

    //E是未知的数据类型
    public class ArrayList<E> {
        public boolean add(E e) {}
        public E get(int index) {}
    }
    
    
    
    //创建集合对象的时候,就会确定泛型的数据类型(会把数据类型作为参数传递)
    public class ArrayList<String> {
        public boolean add(String e) {}
        public String get(int index) {}
    }
    
    public class ArrayList<Student> {
        public boolean add(Studnet e) {}
        public Student get(int index) {}
    }
    

使用泛型的好处

  • 创建集合对象,不使用泛型

    好处:集合不使用泛型,默认的类型就是Object类型,可以存储任意类型的数据

    弊端:不安全,会出现异常

    public class Demo05 {
        public static void main(String[] args) {
            show01();
        }
    
        public static void show01() {
            ArrayList list = new ArrayList<>();
            list.add("abc");
            list.add("def");
            list.add(5);
    
            Iterator iter = list.iterator();
            
            while(iter.hasNext()) {
               Object obj = iter.next();
               String str = (String)obj;//ClassCastException,当iter.next()为数字时,这里不可转为String类型,所以报错
               System.out.println(str);
               System.out.println(str.length());
            }
    
        }
    }
    
  • 创建集合对象,使用泛型

    好处:

    1. 避免了类型转换的麻烦,存储的是什么类型,取出的就是什么类型
    2. 把运行期的异常(代码运行之后会抛出的异常),提升到了编译期(写代码时会报错)

    弊端:

    ​ 泛型是什么类型,只能存储什么类型的数据

    public class Demo06 {
        public static void main(String[] args) {
            show02();
        }
    
        public static void show02() {
            ArrayList<String> list = new ArrayList<>();
            list.add("abc");
            list.add("peach");
    
            Iterator<String> iter = list.iterator();
            while(iter.hasNext()) {
                String str = iter.next();
                System.out.println(str);
                System.out.println("len = " + str.length());
            }
        }
    }
    /*
    output:
    abc
    len = 3
    peach
    len = 5
    */
    

含有泛型的类

  • 泛型的定义

    public class Demo07<E> {
        private E name;
    
        public E getName() {
            return name;
        }
    
        public void setName(E name) {
            this.name = name;
        }
    }
    
  • 泛型的使用

    在创建对象的时候确定泛型

    public class Demo08 {
        public static void main(String[] args) {
            Demo07 nameUnknown = new Demo07();//创建对象时没有确定泛型
            nameUnknown.setName("Mike");
            Object newName = nameUnknown.getName();//不是多态,当返回泛型变量时就会返回Object类的变量
            System.out.println(newName);
    
            Demo07<String> name = new Demo07<>();//创建对象时确定为String
            name.setName("Nick");
            System.out.println(name.getName());
    
            Demo07<Integer> nameNum = new Demo07<>();//创建对象时确定为Integer
            nameNum.setName(57);
            System.out.println(nameNum.getName());
        }
    }
    /*
    output:
    Mike
    Nick
    57
    */
    

含有泛型的方法

  • 定义含有泛型的方法:泛型定义在方法的修饰符和返回值类型之间

  • 格式:

    修饰符 <泛型> 返回值类型 方法名(参数列表(使用泛型)){
    	方法体;
    }
    
  • 含有泛型的方法,在调用方法的时候确定泛型的数据类型

    传递什么类型的参数,泛型就是什么类型

//含有泛型的静态方法,使用类名.方法创建,传递什么类型参数,泛型就是什么类型
public static <E> void genericClass(E e) {
    System.out.println(e);
}
//含有泛型的普通方法,使用new创建对象再访问方法,传递什么类型参数,泛型就是什么类型
public <E> void genericClass2(E e) {
    System.out.println(e);
}

含有泛型的接口

  • 第一种:定义接口的实现类,实现接口,指定接口的泛型

    public interface Iterator<E> {
        E next();
    }
    

    实现类中实现类名不用加泛型,实现的接口要加泛型的具体类型

    //Scanner类实现了Iterator接口,并指定接口的泛型为String,所以重写的next方法泛型默认就是String
    public final class Scanner implements Iterator<String> {
    	public String next() {
    	}
    }
    

    在实现类中确定泛型是什么类型,然后在实例化实现类可以不写泛型的部分,若使用多态方式用接口变量指向实现类对象,最好要写泛型,否则用到的泛型是Object类型

    //InterfaceGeneric<String> ig2= new InterfaceGenericImpl();//多态的写法也可以
    InterfaceGenericImpl ig = new InterfaceGenericImpl();
    String str = ig.printInfo();
    System.out.println(ig.printInfo());
    System.out.println(ig.printString("Apple pen"));
    
  • 第二种:含有泛型的接口第二种使用方式:接口使用什么泛型,实现类就使用什么泛型,类跟着接口走

    就相当于定义了一个含有泛型的类,创建对象的时候确定泛型的类型

    public interface List<E> {
        boolean add(E e);
        E get(int index);
    }
    

    在实现类中实现类的类名后要加泛型,实现的接口后面也要加泛型

    public class ArrayList<E> implements List<E> {
      public boolean add(E e) {}
        public E get(int index) {}
    }
    

    在实例化实现类时确定泛型的类型,所以实例化时类名后最好加泛型的具体类型

    //InterfaceGeneric<String> ig2 = new InterfaceGenericImpl2();//多态写法也可以
    InterfaceGenericImpl2<String> ig2 = new InterfaceGenericImpl2();
    String str2 = ig2.printInfo();
    System.out.println(ig2.printInfo());
    System.out.println(ig2.printString("Banana"));
    

泛型通配符

  • 当使用泛型类或者接口时,传递参数的时候,泛型类型不确定,可以通过通配符<?>表示。但是一旦使用泛型的通配符后,只能使用Object类中的共性方法,集合中元素自身方法无法使用

  • 此时只能接收数据,不能往集合中存储数据

  • 泛型通配符:

    ?代表任意的数据类型

  • 使用方式:不能创建对象使用,只能作为方法的参数传递使用<?>(方法传参不能使用,因为泛型没有继承的概念)

    public class Demo09 {
        public static void main(String[] args) {
            ArrayList<String> list = new ArrayList<>();
            list.add("Apple");
            list.add("Banana");
            ArrayList<Integer> list2 = new ArrayList<>();
            list2.add(45);
            list2.add(23);
    
            printArrayList(list);
            printArrayList(list2);
    
            printArrayList2(list);
            printArrayList2(list2);
        }
    
        public static void printArrayList(ArrayList<?> list) {
            Iterator<?> iter = list.iterator();
            while(iter.hasNext()) {
                System.out.println(iter.next());
            }
        }
    
        public static void printArrayList2(ArrayList<?> list) {
            for(Object i : list) {
                System.out.println(i);
            }
        }
    
    }
    
    • 泛型的上限限定:类名<? extends E> 代表使用的泛型只能是E类型的子类/本身

      泛型的下限限定:类名<? super E> 代表使用的泛型只能是E类型的父类/本身

    三、List、Set、Collections集合类

    List接口

    • Java.util.List接口extends Collection接口

    • List接口的特点:

      1. 有序的集合,存储元素和取出元素的顺序是一致的
      2. 有索引,包含了一些带索引的方法
      3. 允许存储重复的元素
    • List接口中带索引的方法(特有)

      public void add(int index, E element) 将指定的元素,添加到该集合中的指定位置上

      public E get(int index) 返回集合中指定位置的元素

      public E remove(int index) 移除列表中指定位置的元素,返回的是被移除的元素

      public E set(int index, E element) 用指定元素替换集合中指定位置的元素

      注意事项:操作索引的时候,一定要防止索引越界异常

    ArrayList集合

    • java.util.ArrayList集合数据存储的结构是数组结构。
    • 元素增删慢,查找快。常用于查询,遍历数据。
    • ArrayList是不同步的。

    LinkedList集合

    • java.util.LinkedList集合implements List接口

    • LinkedList集合的特点:

      1. 底层是一个链表结构:查询慢,增删快
      2. 里面包含了大量的操作首尾元素的方法
      3. 不同步的

      注意:使用LinkedList集合特有的方法,不能使用多态

    • 特有方法:

      public void addFirst(E e)将指定元素插入此列表的开头

      public void addLast(E e)将指定元素添加到此列表的结尾

      public void push(E e)将元素推入此列表的的堆栈,此方法等效于addFirst(E e)


      public E getFirst()获取此列表的第一个元素

      public E getLast()获取此列表的最后一个元素

      使用这两个方法最好要使用isEmpty()判断,若中间有使用clear()或是LinkedList为空会报错


      public E removeFirst()移除并返回此列表的第一个元素

      public E removeLast()移除并返回此列表的最后一个元素

      public E pop()从列表所表示的堆栈处弹出一个元素,此方法相当于removeFirst()

    Vector集合

    • Vector集合是JDK1.0版本最早期的集合,底层实现也是数组,从JDK2.0开始也实现了List接口。Vector是同步的。

    Set集合

    • java.util.Set接口extends Collection接口
    • Set接口的特点:
      1. 不允许存储重复的元素
      2. 没有索引,没有带索引的方法,也不能使用普通的for循环遍历

    HashSet集合

    • java.util.HashSet集合implements Set接口

    • HashSet特点:

      1. 不允许存储重复的元素
      2. 没有索引,没有带索引的方法,也不能使用普通的for循环遍历
      3. 是一个无序的集合,存储元素和取出元素的顺序可能不一致
      4. 底层是一个哈希表结构(查询速度非常快)
    • 示例:

      //HashSet中没有索引,无法使用普通for循环遍历
      public class Demo01 {
          public static void main(String[] args) {
              Set<Integer> set = new HashSet<>();//多态
              set.add(4);
              set.add(3);
              set.add(2);
              set.add(1);
              set.add(4);//重复元素无法插入
              System.out.println("set.size() = " + set.size());
      		//迭代器遍历,HashSet是无序集合,遍历出顺序与插入时可能不一样
              Iterator<Integer> iter = set.iterator();
              while(iter.hasNext()) {
                  System.out.println(iter.next());
              }
              System.out.println("-------------");
      		//增强for循环遍历,HashSet是无序集合,遍历出顺序与插入时可能不一样
              for(int i : set) {
                  System.out.println(i);
              }
          }
      }
      /*
      output:
      set.size() = 4
      1
      2
      3
      4
      -------------
      1
      2
      3
      4
      */
      

    哈希值

    • 哈希值是一个十进制的整数,由系统随机给出(就是对象的地址值,是一个逻辑地址,是模拟出来的地址,不是数据实际存储的物理地址)

    • 在Object类中有一个方法,可以获取对象的哈希值

      int hashCode()返回对象的哈希码值

      hashCode()方法的源码:

      public native int hashCode();

      native表示该方法调用的是本地操作系统的方法

    • 示例

      public class Person {
          @Override
          public int hashCode() {
              return 1;
          }
      }
      
      public class Demo02 {
          public static void main(String[] args) {
              //Person中重写了hashCode方法,但==比较的是两个变量真实的物理地址值
              Person p1 = new Person();
              Person p2 = new Person();
              System.out.println(p1);
              System.out.println(p2);
              System.out.println(p1.hashCode());
              System.out.println(p2.hashCode());
              System.out.println(p1 == p2);
      
              //String类中重写了Object类中hashCode方法,
              //字符串内容相同就有相同的哈希值,但真实的物理存储地址值不同,因为都是新new的String类对象
              String str1 = new String("apple");
              String str2 = new String("apple");
              System.out.println(str1.hashCode());
              System.out.println(str2.hashCode());
              System.out.println(str1 == str2);
              
      		//特殊巧合的哈希值相同的情况
              System.out.println("通话".hashCode());
              System.out.println("重地".hashCode());
          }
      
      }
      /*
      JavaAdvance.Day03ListSetCollection.Person@1
      JavaAdvance.Day03ListSetCollection.Person@1
      1
      1
      false
      93029210
      93029210
      false
      1179395
      1179395
      */
      

    HashSet集合存储数据的结构

    • HashSet集合存储数据的结构是哈希表(HashTable)

    • JDK1.8版本之前:

      哈希表 = 数组 + 链表

      JDK1.8版本之后:

      哈希表 = 数组 + 链表

      哈希表 = 数组 + 红黑树(提高查询速度)

    • 数据结构:把元素进行了分组,(相同哈希值的元素是一组)

      链表/红黑树结构把相同哈希值的元素连接到一起

    • 哈希冲突:两个元素不同,但是哈希值相同,如"重地"和“通话”

    • 如果链表的长度超过了8位,那么就会把链表转换为红黑树(提高查询的速度)

    Set集合不允许存储重复元素的原理

    • 前提:存储的元素必须重写hashCode方法和equals方法

    • 示例:

      public class Demo03 {
          public static void main(String[] args) {
              HashSet<String> set = new HashSet<>();
              //str1和str2的字符串内容相同,有相同的哈希值,将str1先存储到集合中,当使用str2调用hashCode方法,发现集合中有有这个哈希值,所以哈希冲突。str2.squals(str1)结果是true认定两个元素相同,就不会把str2存储到集合中
              String str1 = new String("abc");
              String str2 = new String("abc");
              set.add(str1);
              set.add(str2);
              //“重地”和“通话”这两个字符串是特殊情况,有相同的哈希值。将“重地”存储到集合中,当"通话"调用hashCode方法,发现集合中有这个哈希值,所以哈希冲突。虽然两个元素哈希值相同,但“通话”调用equals方法返回false认定两个元素不同,就会把“通话”也存储到集合中
              set.add("重地");
              set.add("通话");
              set.add("abc");
              System.out.println(set);
          }
      }
      /*
      [重地, 通话, abc]
      */
      

    HashSet存储自定义类型的元素

    • Set集合存储的元素唯一:

      存储的元素(String, Integer, ... , Student, Person), 必须重写hashCode方法和equals方法

    • 示例:

      假设要求集合中同名同年龄的人,视为一个人,只能存储一次

      //Person类中重写了toString方法、hashCode方法、equals方法
      public class Person {
          private String name;
          private int age;
      
          public Person() {
          }
      
          public Person(String name, int age) {
              this.name = name;
              this.age = age;
          }
      
          @Override
          public boolean equals(Object o) {
              if (this == o) return true;
              if (o == null || getClass() != o.getClass()) return false;
              Person person = (Person) o;
              return age == person.age && Objects.equals(name, person.name);
          }
      
          @Override
          public int hashCode() {
              return Objects.hash(name, age);
          }
      
          @Override
          public String toString() {
              return "Person{" +
                      "name='" + name + '\'' +
                      ", age=" + age +
                      '}';
          }
      
          public String getName() {
              return name;
          }
      
          public void setName(String name) {
              this.name = name;
          }
      
          public int getAge() {
              return age;
          }
      
          public void setAge(int age) {
              this.age = age;
          }
      }
      
      //重写toString方法用于格式化打印Person实例化对象的成员变量
      //重写hashCode方法和equals方法后这两种方法返回值取决于Person实例化对象中的成员变量
      
      //equals未重写(就是用==实现的)作用和==相同,都是比较对象的地址值
      //hashCode未重写,返回值与对象的成员变量无关,是由系统随机给出(就是对象的地址值,是一个逻辑地址,是模拟出来的地址,不是数据实际存储的物理地址)
      public class Demo04 {
          public static void main(String[] args) {
              Person p1 = new Person("Nick", 35);
              Person p2 = new Person("Nick", 35);
              Person p3 = new Person("Jeff", 23);
      
              System.out.println(p1.hashCode());
              System.out.println(p2.hashCode());
              System.out.println(p3.hashCode());
      
              System.out.println(p1 == p2);//p1和p2的地址值不同
              System.out.println(p1.equals(p2));
              System.out.println(p1 == p3);//p1和p3的地址值不同
              System.out.println(p1.equals(p3));
      		
              //p1和p2的hashCode和equals返回值都是相同的,就不会再把p2添加到集合中去了
              HashSet<Person> set = new HashSet<>();
              set.add(p1);
              set.add(p2);
              set.add(p3);
              System.out.println(set);
          }
      }
      /*
      75262145
      75262145
      71451613
      false
      true
      false
      false
      [Person{name='Nick', age=35}, Person{name='Jeff', age=23}]
      */
      

    LinkedHashSet集合

    • java.util.LinkedHashSet集合 extends HashSet集合

    • LinkedHashSet的特点:

      底层是一个哈希表(数组+链表/红黑树)+链表

      多了一条链表(记录元素的存储顺序),保证元素有序

    • 示例:

      public class Demo05 {
          public static void main(String[] args) {
              //HashSet中的元素是无序且不允许重复的
              HashSet<String> set =  new HashSet<>();
              set.add("apple");
              set.add("peach");
              set.add("banana");
              Iterator<String> iter = set.iterator();
              while(iter.hasNext()) {
                  System.out.println(iter.next());
              }
      		
              //LinkedHashSet中的元素是有序且不允许重复的
              LinkedHashSet<String> lSet = new LinkedHashSet<>();
              lSet.add("apple");
              lSet.add("peach");
              lSet.add("banana");
              Iterator<String> iter2 = lSet.iterator();
              while(iter2.hasNext()) {
                  System.out.println(iter2.next());
              }
          }
      }
      /*
      banana
      apple
      peach
      apple
      peach
      banana
      */
      

    可变参数

    • 可变参数是JDK1.5之后出现的新特性

    • 使用前提:当方法的参数列表数据类型已经确定,但是参数的个数不确定,就可以使用可变参数

    • 使用格式(定义方法时使用):

      修饰符 返回值类型 方法名(数据类型...变量名){}

    • 可变参数的原理:可变参数底层就是一个数组,根据传递参数个数的不同,会创建不同长度的数组,来存储这些参数。传递的参数个数,可以是0个(不传递), 1, 2...多个

    • 可变参数的注意事项:

      1. 一个方法的参数列表,只能有一个可变参数
      2. 如果方法的参数有多个,那么可变参数必须写在参数列表的末尾
      public static void method(String b, double c, int d, int ...a)
      
    • 可变参数的终极写法

      public static void method(Object...arr)
      
    • 示例:

      public class Demo06 {
          public static void main(String[] args) {
              System.out.println(add(1, 2, 3, 4, 5));
              System.out.println(add2(1, 2, 3, 4, 5));
          }
      
          public static int add(int...arr) {
              int sum = 0;
      
              for(int i : arr) {
                  sum += i;
              }
              return sum;
          }
          
          public static int add2(int...arr) {
              return arr[0] + arr[1];
          }
      }
      /*
      output:
      15
      3
      */
      

    Collections集合工具类

    • java.utils.Collections是集合工具类,用来对集合进行操作
    • public static boolean addAll(Collection c , T... elements)往集合中添加一些元素
    • public static void shuffle(List<?> list)打乱集合的顺序
    public class Demo07 {
        public static void main(String[] args) {
            ArrayList<Integer> list = new ArrayList<>();
            Collections.addAll(list, 1, 2, 4, 5, 7, 8);
            System.out.println(list);
    
            Collections.shuffle(list);
            System.out.println(list);
         }
    }
    /*
    [1, 2, 4, 5, 7, 8]
    [2, 8, 5, 7, 4, 1]
    */
    
    • public static void sort(List list)将集合中的元素按照默认的规则排序

      注意:sort(List list)使用前提:被排序的集合里边存储的元素,必须实现Comparable,重写接口中的方法compareTo定义排序的规则,如String, Integer中已经重写了compareTo方法(默认按照升序排序)

      Comparable接口的排序规则:

      升序:自己(this)- 参数

      降序:参数 - 自己(this)

    public class Demo08 {
        public static void main(String[] args) {
            ArrayList<Integer> intList = new ArrayList<>();
            Collections.addAll(intList, 32, 23, 54, 3, 12, 27, 45);
            System.out.println(intList);
            ArrayList<String> list =  new ArrayList<>();
            Collections.addAll(list, "a", "d", "h", "e");
            System.out.println(list);
            ArrayList<Person> pList = new ArrayList<>();
            pList.add(new Person("Nick", 45));
            pList.add(new Person("Jeff", 12));
            pList.add(new Person("Mohammed", 27));
            System.out.println(pList);
    
            Collections.sort(list);
            System.out.println(list);
            Collections.sort(intList);
            System.out.println(intList);
            Collections.sort(pList);
            System.out.println(pList);
        }
    }
    /*
    [32, 23, 54, 3, 12, 27, 45]
    [a, d, h, e]
    [Person{name='Nick', age=45}, Person{name='Jeff', age=12}, Person{name='Mohammed', age=27}]
    [a, d, e, h]
    [3, 12, 23, 27, 32, 45, 54]
    [Person{name='Jeff', age=12}, Person{name='Mohammed', age=27}, Person{name='Nick', age=45}]
    */
    
    //Person中重写的compareTo方法按照成员变量age的大小排序,this-args是升序排序
    @Override
    public int compareTo(Person o) {
        return this.age - o.age;
    }
    
    • public static void sort(List list, Comparator<? super T>)将集合中元素按照指定的规则排序

    • Comparator 和 Comparable的区别

      • Comparable:自己(this)和别人(参数)比较,自己需要实现Comparable接口,重写比较规则comparaTo方法

      • Comparator:相当于找了一个第三方的裁判,比较两个,sort第二个参数传入重写compare方法后的Comparator接口的实例化对象

      public class Demo09 {
          public static void main(String[] args) {
              ArrayList<Person> list = new ArrayList<>();
              list.add(new Person("Nick", 45));
              list.add(new Person("Jeff", 23));
              list.add(new Person("Nancy", 66));
      
              System.out.println(list);
      
              Collections.sort(list, new Comparator<Person>() {//匿名内部类
                  @Override
                  public int compare(Person o1, Person o2) {
                      return o1.getAge() - o2.getAge();
                  }
              });
      
              System.out.println(list);
          }
      }
      

    四、集合中常用的数据结构

    数组

    • 数组的特点是:

      查询快:数组的地址是连续的,通过数组的首地址可以找到数组,通过数组的索引可以快速查找某一个元素

      增删慢:数组的长度是固定的,我们想增加或者删除一个元素,必须创建一个新数组,把源数组的数据复制过来

      (举例:要把数组索引是3的元素删除,必须创建一个新的数组,长度是源数组的长度-1,把源数组的其他元素复制到新数组中,再把指向源数组的指针指向新数组,源数组会在内存中被销毁(垃圾回收))

    • Java中数组的增删操作是在堆内存中,频繁地创建数组,复制数组中的元素,销毁数组,效率低下

    链表

    • 链表的特点是:

      查询慢:链表的地址不是连续的,每次查询元素都必须从头开始查询

      增删快:链表结构增加或删除一个元素,对链表整体的结构没有影响,所以增删快

    • 链表的结构:链表中的每一个元素也称为一个节点,一个节点包含了一个数据域(存储数组),两个指针域(存储地址)

    • 单向链表:链表中只有一个链子,不能保证元素的顺序(存储元素和取出元素的顺序有可能不一致)

      双向链表:链表中有两条链子,有一条链子是专门记录元素的顺序,是一个有序集合

    红黑树

    二叉树:分支不能超过两个

    排序树/查找树:在二叉树的基础上,元素是有大小顺序的,左子树小,右子树大

    平衡树:左孩子和有孩子相等

    不平衡树:左孩子与右孩子不相等

    红黑树:趋近于平衡树,查询的速度非常快,查询叶子节点最大次数和最小次数不能超过2倍

    红黑树约束条件:

    1. 根节点是黑色的
    2. 节点可以是红色或黑色的
    3. 叶子节点(空节点)是黑色的
    4. 每个红色的节点的子节点都是黑色的
    5. 任何一个节点到其每个叶子节点的所有路径上黑色节点数相同

    五、Map

    Map集合概述

    • Collection接口定义了单列集合规范,每次存储一个元素(单个元素)

    • Map接口定义了双列集合的规范,每次存储一对儿元素,Map<K, V> ,K代表键的类型,V代表值的类型

    • public interface Map<K, V>

      将键映射到值的对象。一个映射不能包含重复的键(键是唯一的);每个键最多只能映射到一个值(键和值是一一对应的)

    • Map集合的特点:

      1. Map集合是一个双列集合,一个元素包含两个值(一个key,一个value)
      2. Map集合中的元素,key和value的数据类型可以相同,也可以不同
      3. Map集合中的元素,key不允许重复,value是可以重复的
      4. Map集合中的元素,key和value是一一对应的

    Map常用子类

    • java.util.HashMap<k, v>集合implements Map<k, v>接口

      HashMap集合的特点:

      1. HashMap集合底层是哈希表:查询速度特别快

        JDK1.8之前:数组+单向链表

        JDK1.8之后:数组+单向链表/红黑树(链表的长度超过8):提高查询的速度

      2. HashMap集合是一个无序的集合,存储元素和取出元素的顺序可能不一致

    • java.util.LinkedHashMao<k, v>集合extends HashMap集合

      LinkedHashMap的特点:

      1. LinkedHashMap集合底层是哈希表+链表(保证迭代的顺序)
      2. LinkedHashMap集合是一个有序的集合,存储元素和取出元素的顺序是一致的

    Map集合中的常用方法

    • public V put(K key, V value): 把指定的键与指定的值添加到Map集合中

      返回值: v

      存储键值对的时候,key不重复,返回值v是null

      存储键值对的时候,key重复,会使用新的value替换map中重复的value,返回被替换的原value值

      若value是int类型,用Integer类型去接收返回值

    • public v remove(Object key):把指定的键 所对应的键值对元素 在Map集合中删除,返回被删除元素的值

      返回值:v

      key存在,v返回被删除的值

      key不存在,v返回null

      若value是int类型,用Integer类型去接收返回值

    • public V get(Obejct key)根据指定的键,在Map集合中获取对应的值

      返回值:v

      key 存在,返回对应的value值

      key不存在,返回null

      若value是int类型,用Integer类型去接收返回值

    • boolean containsKey(Obejct key)

      判断集合中是否包含指定的键

      包含返回true,不包含返回false

    • 示例Code

      public class Demo01 {
          public static void main(String[] args) {
              Map<String, Integer> map = new HashMap<>();
              map.put("Kim", 168);
              map.put("Mike", 172);
              map.put("Jeff", 165);
              Integer rtMapPut = map.put("Mike", 165);
              System.out.println(rtMapPut);
              System.out.println(map);
      
              Integer rtMapRemove = map.remove("Mike");
              System.out.println(rtMapRemove);
              System.out.println(map);
      
              Integer rtMapGet = map.get("Mike");
              System.out.println(rtMapGet);
              Integer rtMapGet2 = map.get("Kim");
              System.out.println(rtMapGet2);
      
              boolean rtCK = map.containsKey("Kim");
              System.out.println(rtCK);
              boolean rtCK2 = map.containsKey("Mike");
              System.out.println(rtCK2);
          }
      }
      /*
      output:
      172
      {Mike=165, Jeff=165, Kim=168}
      165
      {Jeff=165, Kim=168}
      null
      168
      true
      false
      */
      

    Map集合遍历键找值方式

    • Map集合的第一种遍历方式:通过键找值的方式

      • Map集合中的方法:

        Set keySet()返回此映射中包含的键的Set视图

      • 使用步骤:

        1. 使用Map集合中的方法keySet(),把Map集合中所有的key取出来,存储到一个Set集合中
        2. 遍历set集合,获取Map集合中的每一个key
        3. 通过Map集合中的方法get(key), 通过key获取value
      • 示例Code

        public class Demo02 {
            public static void main(String[] args) {
                traverseMap01();
                traverseMap02();
            }
        
            public static void traverseMap01() {
                Map<String, Integer> map = new HashMap<>();
                map.put("Kimmy", 34);
                map.put("King", 42);
                map.put("Bill", 12);
                Set<String> set = map.keySet();
                Iterator<String> iter = set.iterator();
                while(iter.hasNext()) {
                    String str = iter.next();
                    Integer age = map.get(str);
                    System.out.println("name = " + str + ", age = " +age);
                }
            }
        
            public static void traverseMap02() {
                Map<String, Integer> map = new HashMap<>();
                map.put("Kimmy", 34);
                map.put("King", 42);
                map.put("Bill", 12);
                Set<String> set = map.keySet();
                for(String str : set) {
                    Integer age = map.get(str);
                    System.out.println("name = " + str + ", age = " + age);
                }
            }
        }
        /*
        name = King, age = 42
        name = Bill, age = 12
        name = Kimmy, age = 34
        name = King, age = 42
        name = Bill, age = 12
        name = Kimmy, age = 34
        */
        

    Entry键值对对象

    • Map.Entry<K, V>:在Map接口中有一个内部接口Entry

      作用:当Map集合一创建,那么就会在Map集合中创建一个Entry对象,用来记录键与值(键值对对象,键与值的映射关系)

    • Set<Map.Entry<K, V>> entrySet()

      把Map集合内部的多个Entry对象取出来,存储到一个Set集合中。遍历Set集合,获取Set集合中的每一个Entry对象。使用Entry对象中的方法getKey()获取key,getValue()获取value。

    Map集合遍历键值对的方式

    • Map集合的第二种遍历方式:使用Entry对象遍历

    • Map集合中的方法:

      Set<Map.Entry<K, V>> entrySet()返回此映射中包含的映射关系的Set视图

    • 实现步骤:

      1. 使用Map集合中的方法entrySet(),把Map集合中多个Entry对象取出来,存储到一个Set集合中
      2. 遍历Set集合,获取每一个Entry对象
      3. 使用Entry对象中的方法getKey()和getValue()获取键与值
    • 示例Code:

      public class Demo03 {
          public static void main(String[] args) {
              traverseMap01();
              System.out.println("==========");
              traverseMap02();
          }
      
          public static void traverseMap02() {
              Map<String, Integer> map = new HashMap<>();
              map.put("Kim", 32);
              map.put("Jeff", 40);
              map.put("Mohammed", 16);
              map.put("Rocky", 2);
      
              Set<Map.Entry<String, Integer>> set = map.entrySet();
              Iterator<Map.Entry<String, Integer>> iter = set.iterator();
              while(iter.hasNext()) {
                  Map.Entry<String, Integer> entry = iter.next();
                  String mapKey = entry.getKey();
                  Integer mapValue = entry.getValue();
                  System.out.println("key = " + mapKey + ", value = " + mapValue);
              }
          }
      
          public static void traverseMap01() {
              Map<String, Integer> map = new HashMap<>();
              map.put("Kim", 32);
              map.put("Jeff", 40);
              map.put("Mohammed", 16);
              map.put("Rocky", 2);
      
              Set<Map.Entry<String, Integer>> set = map.entrySet();
              for(Map.Entry<String, Integer> i : set) {
                  String mapKey = i.getKey();
                  Integer mapValue = i.getValue();
                  System.out.println("key = " + mapKey + ", value = " + mapValue);
              }
          }
      }
      /*
      key = Rocky, value = 2
      key = Jeff, value = 40
      key = Mohammed, value = 16
      key = Kim, value = 32
      ==========
      key = Rocky, value = 2
      key = Jeff, value = 40
      key = Mohammed, value = 16
      key = Kim, value = 32
      */
      

    HashMap存储自定义类型键值

    • Map集合要保证key是唯一的

      作为key的元素,则必须重写hashCode和equals方法,以保证key唯一

      public class Demo04 {
          public static void main(String[] args) {
            hashMapTraverse();
            System.out.println("========");
            hashMapTraverse02();
          }
      
          public static void hashMapTraverse02() {
              //key值为Person类型,String类内已经重写了hashCode和equals方法,可以保证put入的key都是唯一的。value值可以重复(重复表示各个成员变量都相同的两个对象),不用重写hashCode和equals方法
              HashMap<Person, String> map = new HashMap<>();
              map.put(new Person(45, "Kim"), "police");
              map.put(new Person(32, "Jeff"), "hunter");
              map.put(new Person(56, "Mohammed"), "thief");
              map.put(new Person(56, "Mohammed"), "thief");
      
              Set<Map.Entry<Person, String>> set = map.entrySet();
              for(Map.Entry<Person, String> i : set) {
                  Person key = i.getKey();
                  String value = i.getValue();
                  System.out.println("key = " + key + ", value = " + value);
              }
          }
      
          public static void hashMapTraverse() {
              //key值为String类型,String类内已经重写了hashCode和equals方法,可以保证put入的key都是唯一的。value值可以重复(重复表示各个成员变量都相同的两个对象),不用重写hashCode和equals方法
              HashMap<String, Person> map = new HashMap<>();
              map.put("police", new Person(45, "Kim"));
              map.put("hunter", new Person(32, "Jeff"));
              map.put("thief", new Person(56, "Mohammed"));
              map.put("thief", new Person(56, "Mohammed"));
      
              Set<String> set = map.keySet();
              Iterator<String> iter = set.iterator();
              while(iter.hasNext()) {
                  String key = iter.next();
                  Person value = map.get(key);
                  System.out.println("key = " + key + ", value = " + value);
              }
          }
      }
      /*
      output:
      key = police, value = Person{age=45, name='Kim'}
      key = thief, value = Person{age=56, name='Mohammed'}
      key = hunter, value = Person{age=32, name='Jeff'}
      ========
      key = Person{age=45, name='Kim'}, value = police
      key = Person{age=56, name='Mohammed'}, value = thief
      key = Person{age=32, name='Jeff'}, value = hunter
      */
      

    LinkedHashMap集合

    • java.util.LinkedHashMap<K, V> extends HashMap<K, V>

      Map接口的哈希表和链接列表实现,具有可预知的迭代顺序

      底层原理:哈希表+链表(记录元素的顺序)

      public class Demo05 {
          public static void main(String[] args) {
              hashMapExample();
              System.out.println("==========");
              linkedHashMapExample();
          }
      
          public static void hashMapExample() {
              //HashMap中元素key是无序的,且不能重复
              HashMap<Integer, Integer> map = new HashMap();
              map.put(5, 2);
              map.put(32, 3);
              map.put(45, 1);
              map.put(12, 3);
              map.put(2, 1);
              map.put(2, 5);
              Set<Integer> set = map.keySet();
              for(Integer i : set) {
                  Integer secNum = map.get(i);
                  System.out.println("key = " + i + ", value = " + secNum);
              }
          }
      
          public static void linkedHashMapExample() {
              //LinkedHashMap中元素key是有序的,且不能重复
              LinkedHashMap<Integer, Integer> map = new LinkedHashMap<>();
              map.put(5, 3);
              map.put(32, 2);
              map.put(45, 3);
              map.put(12, 3);
              map.put(2, 5);
              map.put(2, 3);
              Set<Map.Entry<Integer, Integer>> set = map.entrySet();
              Iterator<Map.Entry<Integer, Integer>> iter = set.iterator();
              while(iter.hasNext()) {
                  Map.Entry<Integer, Integer> entry = iter.next();
                  Integer key = entry.getKey();
                  Integer value = entry.getValue();
                  System.out.println("key = " + key + ", value = " + value);
              }
      
          }
      }
      /*
      key = 32, value = 3
      key = 2, value = 5
      key = 5, value = 2
      key = 12, value = 3
      key = 45, value = 1
      ==========
      key = 5, value = 3
      key = 32, value = 2
      key = 45, value = 3
      key = 12, value = 3
      key = 2, value = 3
      */
      

    HashTable集合

    • java.util.HashTable<K, V>集合 implements Map<K, V>接口

      HashMap的底层是一个哈希表,是一个线程不安全的集合,是多线程的集合,速度快

      HashTable的底层是一个哈希表,是一个线程安全的集合,是单线程集合,速度慢

    • HashMap集合可以存储null键,null值

      HashTable集合不能存储null键,null值

    • HashTable和Vector集合一样,在JDK1.2版本之后被更先进的集合(HashMap, ArrayList)取代了

    • HashTable的子类Properties至今还在使用,Properties集合是唯一一个和IO流相结合的集合

    public class Demo06 {
        public static void main(String[] args) {
            hashMapPrint();
            System.out.println("==========");
            //hashTablePrint();
        }
    
        public static void hashMapPrint() {
            HashMap<String, String> map = new HashMap<>();
            map.put(null, null);
            map.put(null, "a");
            map.put("a", null);
            System.out.println(map);
        }
    
        public static void hashTablePrint() {
            Hashtable<String, String> tab = new Hashtable<>();
            tab.put(null, null);//NullPointerException
            tab.put(null, "b");//NullPointerException
            tab.put("b", null);//NullPointerException
            System.out.println(tab);
        }
    }
    /*
    output:
    {null=a, a=null}
    ==========
    */
    

    JDK9对集合添加元素的优化

    • List接口、Set接口、Map接口:里边增加了一个静态的方法of,可以给集合一次性添加多个元素

      static List of (E... elements)

      使用前提:当集合中存储的元素个数已经确定,不再改变时使用

    • 注意:

      1. of方法只适用于List接口,Map接口,Set接口,不适用于接口的实现类
      2. of方法的返回值是一个不能改变的集合,集合不能再使用add,put方法添加元素,会抛出异常
      3. Set接口和Map接口在调用of方法的时候,不能有重复的元素,否则会抛出异常
    public class Demo08 {
        public static void main(String[] args) {
            //可包含重复元素
            List<Integer> list = List.of(2, 4, 5, 7, 2, 4, 5);
            System.out.println(list);
    		//不可包含重复元素
            Set<Integer> set = Set.of(3, 6, 12, 4);
            System.out.println(set);
    		//不可包含重复元素
            Map<Character, Integer> map = Map.of('a', 1, 'c', 3, 'b', 5);
            System.out.println(map);
        }
    }
    

    六、异常

    异常的概念

    • 异常:指的是程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止

    • 在Java等面向对象语言里,异常本身就是一个类,产生异常就是创建异常对象并抛出了一个异常对象。Java处理异常的方式是中断处理。

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

    • 异常机制是帮助我们找到程序中的问题,异常的根类是java.lang.Throwable,下面有两个子类:

      java.lang.Error与java.lang.Exception,通常指的异常是指java.lang.Exception

    异常的分类

    • Exception:是编译期异常,进行编译(写代码)java程序出现的问题

      RuntimeException(Exception下的子类):运行期间异常,java程序运行过程中出现的问题

      异常相当于是一个小问题,把异常处理掉,程序可以继续执行

      //两种处理异常的方式之一:当parse方法的参数与给定的模式不匹配抛出异常,若匹配则程序正常运行
      public class Demo01 {
          public static void main(String[] args) throws ParseException {
              SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
              Date date = sdf.parse("1999-12-25");
          }
      }
      
      //两种处理异常的方式之一:当parse方法的参数与给定的模式不匹配抛出异常,若匹配则程序正常运行
      public class Demo01 {
          public static void main(String[] args)  {
              SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
              try {
                  Date date = sdf.parse("1999-12-25");
              } catch (ParseException e) {
                  e.printStackTrace();
              }
          }
      }
      
    • Error:是错误不是异常但与Exception都属于Throwable的子类,出现了Error必须修改程序源代码,程序才能执行

      public class Demo01 {
          public static void main(String[] args) {
              int[] arr = new int[1024 * 1024 * 1024];
          }
      }
      /*
      output:
      
      Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
      	at JavaAdvance.Day05ExceptionThread.Demo01.main(Demo01.java:5)
      */
      

    异常的产生过程解析

    throw关键字

    • 作用:使用throw关键字在指定的方法中抛出指定的异常

    • 使用格式:throw new xxxException(“异常产生原因”)

    • 注意事项:

      1. throw关键字必须写在方法的内部

      2. throw关键字后边new的对象必须是Exception或者Exception的子类对象

      3. throw关键字抛出指定的异常对象,就必须要处理这个对象

        • 若throw关键字后边创建的是RuntimeException或者是RuntimeException的子类对象,我们可以不处理,默认交给JVM来处理(打印异常对象,中断程序)
        • 若throw关键字后边创建的是编译异常(写代码时报错),就必须要处理这个异常,要么使用throws要么使用try...catch
      4. 写方法时最好对方法传递过来的参数进行合法性的校验,如果参数不合法,那么我们必须使用抛出异常的方式,告知方法的调用者,传递的参数有问题

        //RuntimeException extends Exception,一般只要throw new抛出不用专门处理,但也可以用throws或try-catch来处理
        public class Demo02 {
            public static void main(String[] args) {
                int[] arr = new int[3];
                getElement(null, 3);
            }
        //NullPointerException是一个运行期间的异常,不用处理,默认交给JVM来处理
            public static int getElement(int[] arr, int index) {
                if(arr == null) {
                    throw new NullPointerException("Pass a null into method");
                }
        //ArrayIndexOutOfBoundsException是一个运行期异常,不用处理,默认交给JVM来处理
                if(index < 0 || index > arr.length-1) {
                    throw new ArrayIndexOutOfBoundsException("Index out of bounds");
                }
                int ele = arr[index];
                return ele;
            }
        }
        

    Objects类中requireNonNull

    • Objects类中的静态方法

      public static <T> T requireNonNull(T obj) {
      	if(obj == null) {
      		throw new nullPointerException();
      	}
      	return obj;
      }
      
    • 示例Code

      public class Demo03 {
          public static void main(String[] args) {
              isNullElement(null, 3);
          }
      
          public static void isNullElement(int[] arr, int num) {
              //用于检测传入参数是否为null
              Objects.requireNonNull(arr);
              //重载(overload)形式的requireNonNull,可包含打印错误信息
              //Objects.requireNonNull(arr, "pass a null value");
          }
      }
      

    异常处理(一):throws关键字

    • throws关键字:异常处理的第一种方式,交给别人处理

    • 作用:当方法内部抛出异常对象时,那么我们就必须处理这个异常对象

      可以使用throws关键字处理异常对象,会把异常对象声明抛出给方法的调用者处理(自己不处理,给别人来处理),最终交给JVM来处理-->中断处理

    • 使用格式:在方法声明时使用

      修饰符 返回值类型 方法名(参数列表)throws AAAException, BBBException... {
      	throw new AAAException(“产生原因”);
      	throw new BBBException(“产生原因”);
      }
      
    • 注意事项:

      1. throws关键字必须写在方法声明处

      2. throws关键字后面声明的异常必须是Exception或者是Exception的子类

      3. 方法内部如果抛出了多个异常对象,那么throws后面必须也声明多个异常

        如果抛出的多个异常对象有子父类的关系,那么直接声明父类异常即可

      4. 调用了一个声明抛出异常的方法,我们就必须处理声明的异常

        要么继续使用throws声明抛出异常对象,让方法的调用者处理,最终交给JVM,最后实行中断处理(该异常之后的代码将不再运行了)

        要么使用try-catch自己处理异常

    • 示例Code

      public class Demo04 {
          //一个方法调用含有异常的方法,也要加该方法中涉及到的异常:使用throws声明抛出这些异常
          public static void main(String[] args) throws IOException {
              readFile("c:\\b.txt");
          }
      
          //FileNotFoundException是IoException的子类,可以只写throws+父类异常,所有异常的父类是Exception
          //FileNotFoundException是编译异常,抛出了编译异常,就必须处理这个异常。可以使用throws继续声明抛出FileNotFoundException这个异常对象,让方法的调用者处理
          public static void readFile(String filename) throws FileNotFoundException, IOException{
      
              if(!filename.endsWith(".txt")) {
              	//格式:throw new 异常类名("异常提示信息")
                  throw new IOException("you did not use a .txt as the rear of filename");
              }
      		
              if(!filename.equals("c:\\a.txt")) {
                  //格式:throw new 异常类名("异常提示信息")
                  throw new FileNotFoundException("you did not input a correct file path");
              }
          }
      }
      

    异常处理(二):try-catch

    • try...catch:异常处理的第二种方式,自己处理异常

    • 格式:

      try {
        	可能产生异常的代码
      } catch(定义一个异常的变量,用来接收try中抛出的异常对象) {
      	异常的处理逻辑,产生异常对象之后,怎么处理异常对象。一般在开发时,会把异常的信息记录到一个日志中
      } catch(异常类名 变量名) {
          
      }
      
    • 注意事项:

      1. try中可能会抛出多个异常对象,那么就可以使用多个catch来处理这些异常对象
      2. 如果try中产生了异常,那么就会执行catch中的异常处理逻辑(跳过try中剩下的代码,不会跳过finally代码块中的内容),执行完毕catch中的处理逻辑,继续执行try...catch之后的代码
      3. 如果try中没有产生异常,那么就不会执行catch中异常的处理逻辑,执行完try中的代码,继续执行try...catch之后的代码
    • 示例Code

      public class Demo04 {
          public static void main(String[] args) {
              try{
                  readFile("c:\\b.txt");
              }catch (IOException e) {//try中抛出什么异常对象,catch就定义什么异常对象,用来接收这个异常对象
                  System.out.println("catch: IoException!");
              }
              System.out.println("after catch...");//catch内代码运行后,继续执行trycatch之后的代码
          }
      
          public static void readFile(String filename) throws FileNotFoundException, IOException{
      
              if(!filename.endsWith(".txt")) {
                  throw new IOException("you did not use a .txt as the rear of filename");
              }
      
              if(!filename.equals("c:\\a.txt")) {
                  throw new FileNotFoundException("you did not input a correct file path");
              }
          }
      }
      /*
      output:
      
      catch: IoException!
      after catch...
      */
      

    Throwable类中的3个异常处理的方法

    • Throwable类中定义了3个异常处理的方法(经常在catch的代码块中使用)

      String getMessage() 返回此throwable的简短描述

      String toString() 返回此throwable的详细消息字符串

      void printStackTrace() JVM打印异常对象,默认此方法,打印的异常信息是最全面的(idea红字显示)

    • 示例Code

      public class Demo04 {
          public static void main(String[] args) {
              try{
                  readFile("c:\\b.txt");
              }catch (IOException e) {
                  System.out.println("catch: IoException!");
                  System.out.println(e.getMessage());
                  System.out.println(e.toString());//重写了Object类中的toString方法,效果同System.out.println(e)
                  e.printStackTrace();//无返回值,直接使用对象调用
              }
              System.out.println("after catch...");
          }
      
          public static void readFile(String filename) throws FileNotFoundException, IOException{
      
              if(!filename.endsWith(".txt")) {
                  throw new IOException("you did not use a .txt as the rear of filename");
              }
      
              if(!filename.equals("c:\\a.txt")) {
                  throw new FileNotFoundException("you did not input a correct file path");
              }
          }
      }
      /*
      output:
      
      catch: IoException!
      you did not input a correct file path
      java.io.FileNotFoundException: you did not input a correct file path
      after catch...
      */
      

    finally代码块

    • 格式:

      try {
        	可能产生异常的代码
      } catch(定义一个异常的变量,用来接收try中抛出的异常对象) {
      	异常的处理逻辑,产生异常对象之后,怎么处理异常对象。一般在开发时,会把异常的信息记录到一个日志中
      } catch(异常类名 变量名) {
          
      } finally {
          无论是否出现异常都会执行
      }
      
    • 注意事项:

      1. finally不能单独使用,必须和try一起使用
      2. finally一般用于资源释放(资源回收),无论程序是否出现异常,最后都要资源释放(IO)
    • 示例Code

      public class Demo04 {
          public static void main(String[] args) {
              try{
                  readFile("c:\\b.txt");
                  System.out.println("try block code after readFile method");//这句始终未打印
              }catch (IOException e) {
                  System.out.println("catch: IoException!");
                  System.out.println(e.getMessage());
                  System.out.println(e.toString());
                  e.printStackTrace();
              } finally {
                  System.out.println("finally block code is running");
              }
              System.out.println("after catch...");
          }
      
          public static void readFile(String filename) throws FileNotFoundException, IOException{
      
              if(!filename.endsWith(".txt")) {
                  throw new IOException("you did not use a .txt as the rear of filename");
              }
      
              if(!filename.equals("c:\\a.txt")) {
                  throw new FileNotFoundException("you did not input a correct file path");
              }
          }
      }
      /*
      output:
      
      catch: IoException!
      you did not input a correct file path
      java.io.FileNotFoundException: you did not input a correct file path
      finally block code is running	//finally代码块的运行位置:在这里运行
      after catch...
      java.io.FileNotFoundException: you did not input a correct file path
      	at JavaAdvance.Day05ExceptionThread.Demo04.readFile(Demo04.java:28)
      	at JavaAdvance.Day05ExceptionThread.Demo04.main(Demo04.java:9)
      */
      

    多异常的捕获处理

    • 多个异常的3种情况:

      1. 多个异常分别处理,有几个异常写几个try-catch

        public static void main(String[] args) {
            try {
                int[] arr = new int[]{1, 2, 3, 4, 5};
                System.out.println(arr[5]);
            } catch (ArrayIndexOutOfBoundsException e) {
                System.out.println(e);
            }
        
            try {
                List<Integer> list = new ArrayList<>();
                list.add(1);
                list.add(3);
                list.add(5);
                list.add(2);
                Integer num = list.get(4);
            } catch(IndexOutOfBoundsException e) {
                System.out.println(e);
            }
        }
        /*
        java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5
        java.lang.IndexOutOfBoundsException: Index 4 out of bounds for length 4
        */
        
      2. 多个异常一次捕获,多次处理,格式是一个try,有几个异常就有几个catch

        //public class ArrayIndexOutOfBoundsException extends IndexOutOfBoundsException
        public class Demo06 {
            public static void main(String[] args) {
                try {
                    int[] arr = new int[]{1, 2, 3, 4, 5};
                    //try中会产生Array溢出的异常对象,将catch住第一个异常
                    System.out.println(arr[5]);
                    List<Integer> list = new ArrayList<>();
                    list.add(1);
                    list.add(3);
                    list.add(5);
                    list.add(2);
                    //假设没有第一个异常对象产生,try中会产生List溢出的异常对象。假设有第一个异常对象,第一个catch完,将退出try-catch,这里第二个异常将跳过
                    Integer num = list.get(4);
                } catch (ArrayIndexOutOfBoundsException e) {
                    System.out.println(e);
                } catch(IndexOutOfBoundsException e) {//若IndexOutOfBoundsException写在前面,既可以接收Array又可以接收List(多态),后面的ArrayIndexOutofBoundsException将不再起作用
                    System.out.println(e);
                }
            }
        }
        /*
        java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5
        */
        

        注意事项:catch里边定义的异常变量,如果有子父类关系,那么子类的异常变量必须写在上边 ,否则就会报错

      3. 多个异常一次捕获一次处理

        public class Demo06 {
            public static void main(String[] args) {
                try {
                    int[] arr = new int[]{1, 2, 3, 4, 5};
                    System.out.println(arr[5]);
                    List<Integer> list = new ArrayList<>();
                    list.add(1);
                    list.add(3);
                    list.add(5);
                    list.add(2);
                    Integer num = list.get(4);
                }  catch(IndexOutOfBoundsException e) {//这里异常类推荐写Exception类,可以捕获大多数的异常情况
                    System.out.println(e);
                }
            }
        }
        /*
        上述代码的输出结果:
        java.lang.ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5
        若没有产生第一个异常对象,则输出结果是:
        java.lang.IndexOutOfBoundsException: Index 4 out of bounds for length 4
        */
        
      4. 上述的举例情况都是运行时异常,运行时异常被抛出可以不处理,既不捕获也不声明抛出

        默认给虚拟机处理,终止程序,什么时候不抛出运行时的异常了,再来执行程序

    finally代码块中带return的情况

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

    • 示例Code

      public class Demo07 {
          public static void main(String[] args) {
              int num = getElement();
              System.out.println(num);
          }
      
          public static int getElement() {
              int a = 10;
              try {
                  return a;
              } catch (Exception e) {
                  System.out.println(e);
              } finally {
                  a = 100;
                  return a;
              }
          }
      }
      /*
      output:
      100
      */
      

    子父类异常

    • 子父类异常:

      • 如果父类抛出了多个异常,子类重写父类方法时,抛出和父类相同的异常或是父类异常的子类或者不抛出异常
      • 父类方法没有抛出异常,子类重写父类方法时也不可抛出异常。此时子类产生独有异常,只能捕获处理,不能声明抛出
    • 注意:父类异常是什么样(声明),子类异常就是什么样(声明)

    • 示例Code:

      public class Parent {
          public  void method01() throws Exception {}
          public  void method02() throws IndexOutOfBoundsException {}
          public  void method03() throws Exception {}
      
          public  void method04() {}
      }
      
      class child {
          public  void method01() throws Exception {}
          public  void method02() throws ArrayIndexOutOfBoundsException {}
          public  void method03() {}
      
          public  void method04() {
              try {
                  throw new Exception();
              } catch (Exception e) {
                  e.printStackTrace();
              }
          }
      }
      

    自定义异常类(异常类的理解总结)

    • Java中提供的异常类,不够我们使用,需要自定义一些异常类

    • 格式:

      //Exception一般是编译期间的异常,一般throw new抛出,要用try-catch或throws来进行异常处理
      
      public class XXXException extends Exception {
          //1.添加一个空参数的构造方法
          //2.再添加一个带异常信息的构造方法
      }
      
      //或者
      
      //RuntimeException extends Exception,一般只要throw new抛出不用专门处理(推荐),但也可以用throws或try-catch来处理(不推荐)
      public class XXXException extends RuntimeException {
          //1.添加一个空参数的构造方法
          //2.再添加一个带异常信息的构造方法
      }
      
    • 注意:

      1. 自定义异常类一般都是以Exception结尾,说明该类是一个异常类
      2. 自定义异常类,必须继承Exception或是RuntimeException
        • 继承Exception:那么自定义的异常类就是一个编译期间的异常,如果方法内部抛出了编译期异常,就必须处理这个异常,要么throws,要么try...catch
        • 继承RuntimeException:那么自定义的异常类就是一个运行期间的异常,无需处理,交给JVM处理(中断处理)
    • 示例Code

      //自定义异常类的常规写法,构造方法(无参或有参)中调用的是父类的构造方法,idea快捷键 Alt + Insert
      public class Demo08Exception extends Exception {
          public Demo08Exception() {
              //默认调用父类空参构造,这里的super()可写可不写
              super();
          }
      	//传入异常打印信息的字符串
          public Demo08Exception(String message) {
              super(message);
          }
      }
      

    七、线程、同步

    并发与并行

    • 并发:指两个或多个事件在同一段时间内交替执行
    • 并行:指两个或多个事件在同一时刻同时发生

    线程

    • 进程是内存中运行的应用程序,每个进程都有独立的内存空间,一个应用程序可以同时运行多个进程。

      进程也是程序的一次执行过程,是系统运行程序的基本单位。系统运行一个程序即是一个进程从创建、运行到消亡的过程

    • 线程是进程中的一个 执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中可以有多个线程,这个程序也可以成为多线程程序。

    线程的调度

    • 分时调度

      所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间

    • 抢占式调度

      优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个(线程的随机性),Java使用的为抢占式调度

    主线程

    • 主线程是执行主方法(main)的线程
    • 主线程也是单线程程序:Java程序中只有一个线程,执行main方法开始,从上依次到下
    • JVM执行main方法,main方法会进入到栈内存,JVM会找操作系统开辟一条main方法通向CPU的执行路径,这个路径就是main(主)线程

    创建线程(一): Thread

    • 创建多线程程序的第一种方法:创建Thread类的子类

      Java.lang.Thread类:是描述线程的类,想要实现多线程的程序,就必须继承Thread类

    • 实现步骤:

      1. 创建一个Thread类的子类(extends Thread)

      2. 在Thread类的子类中重写Thread类中的run方法,设置线程任务(开启线程要执行的任务)

      3. 创建Thread类的子类对象

      4. 调用Thread类中的方法start方法,开启新的线程,执行run方法

        void start()使该线程开始执行,Java虚拟机调用该线程的run方法

        结果是两个线程并发地运行(当前线程(main线程)和另一个线程(创建的新线程,执行其run方法))

        注意:多次启动一个线程是非法的,特别是当线程已经结束执行后,不能再重新启动

      5. Java程序是属于抢占式的调度,哪个线程的优先级高,哪个线程就优先执行,同一个优先级,随机选择一个执行

    • 示例Code

      public class Demo10 extends Thread{
          @Override
          public void run() {
              for (int i = 0; i < 20; i++) {
                  try {
                      Thread.sleep(500);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  System.out.println("run");
              }
          }
      }
      
      public class Demo11 {
          public static void main(String[] args) throws InterruptedException {
              Demo10 demo10 = new Demo10();
              demo10.start();
              for (int i = 0; i < 20; i++) {
                  Thread.sleep(500);
                  System.out.println("main method");
              }
          }
      }
      /*
      output:
      main method
      run
      main method
      run
      main method
      run
      run
      main method
      main method
      run
      main method
      run
      main method
      run
      main method
      run
      main method
      run
      run
      main method
      main method
      run
      main method
      run
      run
      main method
      run
      main method
      run
      main method
      run
      main method
      main method
      run
      main method
      run
      main method
      run
      run
      main method
      */
      
    • 代码图解

    • 内存图解

      注意:

      1. 直接调用Thread类对象run方法和调用start方法间接调用run方法是不同的,前者是单线程的,后者会开辟新的栈内存空间在其中执行run方法
      2. 多线程的好处:多个线程之间互不影响,因为每次调用start方法会在开辟新的栈空间中运行run,这些线程对应的run方法运行在不同的栈空间中,某一时刻谁被运行由CPU选择决定

    Thread类的常用方法

    • 获取线程的名称

      1. 使用Thread类中的方法getName()

        String getName() 返回该线程的名称

      2. 先获取当前正在执行的线程,使用线程中的方法getName()获取线程的名称

        static Thread currentThread()返回当前正在执行的线程对象的引用

      public class ThreadDemo01 extends Thread{
          @Override
          public void run() {
              String threadName = getName();
              System.out.println(threadName);
              System.out.println(ThreadDemo01.currentThread().getName());//ThreadDemo01中返回Thread对象
          }
      }
      
      public class AppDemo01 {
          public static void main(String[] args) {
              new ThreadDemo01().start();//每次调用start方法,打印两次线程名称
              new ThreadDemo01().start();//每次调用start方法,打印两次线程名称
              System.out.println(ThreadDemo01.currentThread().getName());//main中返回main线程的对象
          }
      }
      /*
      output:
      
      main
      Thread-0
      Thread-1
      Thread-1
      Thread-0
      */
      
    • 设置线程的名称

      1. 使用Thread类中的方法setName(名字)

        void setName(String name) 改变线程名称,使之与参数name相同

      2. 创建一个带参数的构造方法,参数传递线程的名称;调用父类的带参构造方法,把线程名称传递给父类,让父类给子类线程起一个名字

        Thread(String name)分配新的Thread对象

      public class ThreadDemo02 extends Thread{
      
          ThreadDemo02() {
      
          }
      
          ThreadDemo02(String name) {
              super(name);
          }
      
          @Override
          public void run() {
              Thread thread = ThreadDemo01.currentThread();
              System.out.println(thread.getName());
          }
      }
      
      public class AppDemo02 {
          public static void main(String[] args) {
              //因为有new ThreadDemo02(),这里虽然改名了,但实际还是thread-0
              ThreadDemo02 threadDemo02 = new ThreadDemo02();
              threadDemo02.setName("thread A");
              threadDemo02.start();
      
              //有参构造方法创建的不算thread-X
              new ThreadDemo02("threadName by constructor").start();
              
              //thread-1
              new ThreadDemo02().start();
      
          }
      }
      /*
      output:
      
      thread A
      threadName by constructor
      Thread-1
      */
      
    • Thread类的常用方法sleep

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

        毫秒数结束后,线程继续执行

      • 示例Code

        public class AppDemo03 {
            public static void main(String[] args) {
                for (int i = 0; i < 20; i++) {
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(i);
                }
            }
        }
        

    创建线程(二): Runnable+Thread

    • 创建多线程程序的第二种方法:实现Runnable接口

      java.lang.Runnable中的Runnable接口应该由那些打算通过某一线程执行实例的类来实现,类必须定义一个称为run的无参方法

      java.lang.Thread类的构造方法中重载形式有:

      Thread(Runnable target) 分配新的Thread对象

      Thread(Runnable target, String name) 分配新的Thread对象

    • 实现步骤:

      1. 创建一个Runnable接口的实现类(implements Runnable)
      2. 在实现类中重写Runnable接口的run方法,设置线程任务
      3. 创建一个Runnable接口的实现类对象
      4. 创建Thread类对象,构造方法中传递Runnable接口的实现类对象
      5. 调用Thread类对象的start方法,在开启的新线程中,执行run方法中的任务
    • 示例Code

      public class ThreadDemo03 implements Runnable{
          public void run() {
              for (int i = 0; i < 20; i++) {
                  try {
                      Thread.sleep(500);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  System.out.println(i);
              }
          }
      }
      
      public class AppDemo04 {
          public static void main(String[] args) {
              ThreadDemo03 threadDemo03 = new ThreadDemo03();
              new Thread(threadDemo03).start();
          }
      }
      

    使用Runnable+Thread创建线程的好处

    • 避免了单继承的局限性

      子类单继承父类:一个类只能继承一个类,类继承了Thread类就不能继承其他的类

      实现类实现接口:实现Runnable接口,还可以继承其他类,实现其他接口

    • 增强了程序的扩展性,降低了程序的耦合性(解耦)

      实现Runnable接口的方式,把设置线程任务和开启新线程进行了分离(解耦)

      实现类中,重写了run方法:用来设置线程任务

      创建Thread类对象,调用start方法:用来开启新线程

      更改线程任务只需改变Runnable的实现类中run方法,Thread类对象调用start无需更改,这样新线程的执行任务,与Thread类再无关系

    使用匿名内部类创建线程

    • 匿名:没有名字

      内部类:写在其他类内部的类

    • 匿名内部类作用:简化代码

      把子类继承父类,重写父类的方法,创建子类对象合成一步来完成

      把实现类实现接口,重写接口中的方法,创建实现类对象合成一步完成

      匿名内部类的最终产物:子类/实现类对象,而这个类没有名字

    • 格式:

      new 父类/接口() {
        重写父类/接口中的方法  
      };
      
    • 示例Code

      public class AppDemo05 {
          public static void main(String[] args) {
              //省略中间变量,直接创建匿名对象+匿名内部类直接重写父类中的run方法
              new ThreadDemo05() {
                  @Override
                  public void run() {
                      for (int i = 0; i < 20; i++) {
                          System.out.println("thread: " + currentThread().getName() + ", i = " + i);
                      }
                  }
              }.start();
      
              System.out.println("=====================");
      		//匿名内部类直接重写接口实现类中的run方法
              Runnable runnable =  new RunnableImpl() {
                  @Override
                  public void run() {
                      for (int i = 0; i < 20; i++) {
                          System.out.println("thread: " + currentThread().getName() + ", j = " + i);
                      }
                  }
              };
              new Thread(runnable).start();
      
              System.out.println("=====================");
      		//省略中间变量,直接创建匿名对象+匿名内部类生成Runnable类型的对象
              new Thread(new RunnableImpl() {
                  @Override
                  public void run() {
                      for (int i = 0; i < 20; i++) {
                          System.out.println("thread: " + currentThread().getName() + ", k = " + i);
                      }
                  }
              }).start();
          }
      }
      

    线程安全问题

    • 多线程访问了共享的数据,会产生线程安全问题;

      多线程没有访问共享的数据,不会产生线程安全问题;

      单线程程序不会出现线程安全问题

    解决线程安全问题

    • 三种常见的解决办法:同步代码块、同步方法、同步锁

    • 同步代码块

      格式:

      synchronized(锁对象) {
          可能会出现线程安全问题的代码(访问了共享数据的代码)
      }
      

      注意事项:

      1. 同步代码块中的锁对象,可以使用任意的对象

      2. 但是必须保证多个线程使用的锁对象是同一个

      3. 锁对象的作用:

        把同步代码块锁住,只让一个线程在同步代码块中执行

      4. 生成锁对象在run方法的外部,保证多个线程所使用的锁对象是同一个

      示例Code:

      public class RunnableImpl implements Runnable{
          public int ticketCnt = 100;
      
          Object object = new Object();//锁对象可以使用任意的对象,但保证多个线程所使用的锁对象是同一个,所以不能在run方法的内部生成
          @Override
          public void run() {
              synchronized (object) {//synchronized代码块包含了所有的ticketCnt变量改变的代码区域(因为ticketCnt是共享数据)
                  while (ticketCnt > 0) {
                      try {
                          sleep(500);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      System.out.println("ThreadInfo : " + currentThread().getName() + " Current ticket num: " + ticketCnt);
                      ticketCnt--;
                  }
              }
          }
      }
      
    • 同步方法

      • 使用步骤:

        1. 把访问了共享数据的代码抽取出来,放到一个方法中
        2. 在方法上添加synchronized修饰符
      • 格式:

        修饰符 synchronized 返回值类型 方法名(参数列表){
            可能会出现线程安全问题的代码(访问了共享数据的代码)
        }
        
      • 同步方法的原理:

        定义了一个同步方法,同步方法也会把方法内部的代码锁住,只让一个线程执行。同步方法的锁对象就是实现类对象new RunnableImpl()也就是this

      • 示例Code

        public class RunnableImpl implements Runnable{
            public int ticketCnt = 100;
        
            @Override
            public void run() {
                synMethod();//就等于this.synMethod()其中this.可以省略
            }
        
            public synchronized void synMethod() {
                while (ticketCnt > 0) {
                    System.out.println("ThreadInfo : " + currentThread().getName() + " Current ticket num: " + ticketCnt);
                    ticketCnt--;
                }
            }
        }
        
      • 静态同步方法

        • 静态同步方法的锁对象不是this,this是创建对象之后产生的,静态方法优先于对象。静态同步方法的锁对象是本类的class属性也就是class文件对象(反射)

          //静态同步方法内部类似于同步代码块(锁对象不能用this,用RunnableImpl.class)
          synchronized(RunnableImpl.class){
              
          }
          
        • 格式:

          public static synchronized void XXXX(){}
          

          注意:静态同步方法中使用的成员变量也必须是static修饰的

    • 同步锁

      • java.util.concurrent.locks.Lock接口

      • Lock实现提供了比使用synchronized方法和语句可获得的更广泛的锁定操作,同步锁比起同步代码块和同步方法来说,更加显式的体现了加锁的位置

      • Lock接口中的方法:

        void lock()

        获取锁

        void unlock()

        释放锁

      • 使用步骤:

        1. 在成员位置创建一个ReentrantLock对象
        2. 在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
        3. 在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁
      • 示例Code

        public class RunnableImpl implements Runnable{
            public int ticketCnt = 100;
            Lock lock = new ReentrantLock();
            @Override
            public void run() {
                synMethod();
            }
        
            public void synMethod() {
                lock.lock();
                while (ticketCnt > 0) {
                    System.out.println("ThreadInfo : " + currentThread().getName() + " Current ticket num: " + ticketCnt);
                    ticketCnt--;
                }
                lock.unlock();
            }
        }
        
    • 同步技术的原理

      • 同步中的线程,没有执行完毕不会释放锁对象,同步外的线程没有锁对象进不去同步

      • 同步保证了只能有一个线程在同步中使用共享数据,保证了安全。但是程序频繁地判断锁,获取锁,释放锁,程序的效率会降低

      • 代码图解

    线程状态

    • Thread类中的内部类Thread.State中定义了线程的六种状态

    • 阻塞状态:具有CPU的执行资格,等待CPU空闲时执行

      休眠状态:放弃CPU的执行资格,CPU空闲,也不执行

    • 线程状态图

    八、等待与唤醒机制、线程池

    等待唤醒案例

    • 等待唤醒案例:线程之间的通信

      创建一个顾客线程(消费者):告知老板要的包子的种类和数量,调用wait方法,放弃CPU的执行,进入到WAITING状态(无限等待)

      创建一个老板线程(生产者):花了5s做包子,包子做好后,调用notify方法,唤醒顾客吃包子

      注意:

      1. 顾客和老板线程必须使用同步代码块包裹起来,保证等待和唤醒只能有一个执行。
      2. 同步使用的锁对象必须保证唯一
      3. 只有锁对象才能调用wait和notify方法
    • Object类中的方法

      void wait()

      在其他线程中调用此对象的notify()方法或notifyAll()方法前,会导致当前线程等待

      void notify()

      唤醒在此对象监视器上等待的单个线程,会继续执行wait方法之后的代码

    • 示例Code

      public class AppDemo07 {
          public static void main(String[] args) {
              Object object = new Object();
              new Thread() {
                  @Override
                  public void run() {
                      while(true) {
                          synchronized (object) {
                              System.out.println("Customer" + currentThread().getName() + ": customer is asking for purchasing...");
                              try {
                                  object.wait();
                              } catch (InterruptedException e) {
                                  e.printStackTrace();
                              }
                              System.out.println("Customer" + currentThread().getName() + ": customer has received products!");
                              System.out.println("This inter-thread communication has been terminated!");
                              System.out.println("================================================");
                          }
                      }
                  }
              }.start();
      
              new Thread() {
                  @Override
                  public void run() {
                      while (true) {
                          try {
                              Thread.sleep(2000);
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
      
                          synchronized (object) {
                              System.out.println("Shopkeeper" + currentThread().getName() + ": shopkeeper is preparing products...");
                              System.out.println("Shopkeeper" + currentThread().getName() + ": after 2s, products has been released.");
                              object.notify();
                          }
                      }
                  }
              }.start();
          }
      }
      /*
      output:
      
      CustomerThread-0: customer is asking for purchasing...
      ShopkeeperThread-1: shopkeeper is preparing products...
      ShopkeeperThread-1: after 2s, products has been released.
      CustomerThread-0: customer has received products!
      This inter-thread communication has been terminated!
      ================================================
      CustomerThread-0: customer is asking for purchasing...
      ShopkeeperThread-1: shopkeeper is preparing products...
      ShopkeeperThread-1: after 2s, products has been released.
      CustomerThread-0: customer has received products!
      This inter-thread communication has been terminated!
      ================================================
      //循环重复打印这些内容...
      
      */
      

    Object类中的wait带参方法和notifyAll方法

    • 进入到TimeWaiting(计时等待)有两种方式:

      1. 使用sleep(long m)方法,在毫秒值结束之后,线程睡醒进入到Runnable/Blocked的状态

      2. 使用wait(long m)方法,wait方法如果在毫秒值结束之后,还没有被notify唤醒,就会自动醒来,线程睡醒进入到Runnable/Blocked状态

      3. 示例Code

        //wait带参的方法同sleep带参的方法,若无notify,则时间到后自动醒来
        //可以循环打印run方法的所有内容
        public class AppDemo08 {
            public static void main(String[] args) {
                Object object = new Object();
                new Thread() {
                    @Override
                    public void run() {
                        while(true) {
                            synchronized (object) {
                                System.out.println("Customer" + currentThread().getName() + ": customer is asking for purchasing...");
                                try {
                                    object.wait(500);
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                                System.out.println("Customer" + currentThread().getName() + ": customer has received products!");
                                System.out.println("This inter-thread communication has been terminated!");
                                System.out.println("================================================");
                            }
                        }
                    }
                }.start();
            }
        }
        
    • 唤醒的方法:

      1. void notify()唤醒在此对象监视器(锁对象)上等待的单个线程

      2. void notifyAll()唤醒在此对象监视器(锁对象)上等待的所有线程

      3. 示例Code

        //多个消费者(在消费者使用wait无参方法),一个生产者(在生产者中使用notifyAll方法)
        //notifyAll每次同时唤醒两个wait的线程
        //notify每次只能随机唤醒一个wait的线程
        public class AppDemo09 {
            public static void main(String[] args) {
                Object object = new Object();
                new Thread() {
                    @Override
                    public void run() {
                        while(true) {
                            synchronized (object) {
                                System.out.println("Customer" + currentThread().getName() + ": customer is asking for purchasing...");
                                try {
                                    object.wait();
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                                System.out.println("Customer" + currentThread().getName() + ": customer has received products!");
                                System.out.println("This inter-thread communication has been terminated!");
                                System.out.println("================================================");
                            }
                        }
                    }
                }.start();
        
                new Thread() {
                    @Override
                    public void run() {
                        while(true) {
                            synchronized (object) {
                                System.out.println("Customer" + currentThread().getName() + ": customer is asking for purchasing...");
                                try {
                                    object.wait();
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                                System.out.println("Customer" + currentThread().getName() + ": customer has received products!");
                                System.out.println("This inter-thread communication has been terminated!");
                                System.out.println("================================================");
                            }
                        }
                    }
                }.start();
        
                new Thread() {
                    @Override
                    public void run() {
                        while (true) {
                            try {
                                Thread.sleep(2000);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
        
                            synchronized (object) {
                                System.out.println("Shopkeeper" + currentThread().getName() + ": shopkeeper is preparing products...");
                                System.out.println("Shopkeeper" + currentThread().getName() + ": after 2s, products has been released.");
                                object.notifyAll();
                            }
                        }
                    }
                }.start();
            }
        }
        

    线程之间的通信

    • 概念:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同

    • 意义:多个线程并发执行时,默认情况下CPU是随机切换线程的,当我们需要有多个线程来共同完成一件任务时,并且希望他们有规律的执行,那么多线程之间需要一些协调通信,从而可以实现多线程共同操作一份数据

    • 保证线程间通信有效利用资源:

      多个线程在操作同一份数据时,要避免对共享变量的争夺,使用等待唤醒机制可以使得各个线程能有效利用资源

    等待唤醒机制

    • 等待唤醒机制是多线程之间的一种协作机制,就是在一个线程进行了规定的操作后进入等待状态(wait()),等待其他线程执行完它们的指定代码后,再将其唤醒(notify()); 在有多个线程进行等待时,如果需要,可以使用notifyAll()来唤醒所有的等待线程

    • wait/notify就是线程中的一种协作机制

    • 等待唤醒机制中的方法

      1. wait:线程不再活动,不再参与调度,进入wait set中,因此不会浪费CPU资源,也不会竞争锁了,这是线程的状态时WAITING,此时这个线程要等待别的线程执行notify,把在这个对象上等待的线程从wait set中释放出来,重新进入到ready queue(调度队列)中
      2. notify:选取所通知对象的wait set中的一个线程释放
      3. notifyAll:释放所通知对象的wait set上的全部线程
    • wait和notify方法的注意事项

      1. wait方法与notify方法必须由同一个锁对象调用。因为对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程
      2. wait与notify方法是属于Object类的方法。因为锁对象可以是任意的对象,而任意对象的所属类都是继承了Object类的
      3. wait方法与notify方法必须要在同步代码块或是同步函数中使用。因为必须要通过锁对象调用这两个方法
    • 示例Code

      public class Consumer extends Thread {
          private Bao bao;
      
          Consumer(Bao bao) {
              this.bao = bao;
          }
      
          public void run() {
              while(true) {
                  synchronized (bao) {
                      if(bao.isHasBao() == false) {
                          try {
                              bao.wait();
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                      }
      
                      System.out.println("customer is eating " + bao.getSkin()+ bao.getStuffing() + "bao");
      
                      bao.setHasBao(false);
      
                      bao.notify();
      
                      System.out.println("customer has had " + bao.getSkin()+ bao.getStuffing() + "bao");
      
                  }
              }
          }
      }
      
      public class Producer extends Thread {
      
          private Bao bao;
      
          Producer(Bao bao) {
              this.bao = bao;
          }
          public void run() {
              int cnt = 0;
              while(true) {
                  synchronized (bao) {
                      if(bao.isHasBao() == true) {
                          try {
                              bao.wait();
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                      }
      
                      if(cnt % 2 == 0) {
                          bao.setStuffing("pork");
                          bao.setSkin("thin");
                      } else {
                          bao.setStuffing("beef");
                          bao.setSkin("thick");
                      }
      
                      cnt++;
      
                      System.out.println("cook is making bao...");
                      try {
                          Thread.sleep(2000);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
      
                      bao.setHasBao(true);
      
                      System.out.println("cook has done.bao is " + bao.getSkin() + bao.getStuffing() + "bao");
      
                      bao.notify();
                  }
              }
      
          }
      }
      

    线程池概念与原理

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

    • 线程池就是一个容器,如LinkedList,线程池添加元素就使用add(new Thread(xxx))

      当程序第一次启动的时候,创建多个线程,保存到一个集合中,当我们想要使用线程的时候,就可以从集合中取出线程来使用

      Thread t = list.remove(0);//返回的是被移除的元素(线程只能被一个任务使用)
      
      Thread t = linked.removeFist//若集合是LinkedList<Thread>,可以使用removeFist来移除第一个元素
      

      当我们使用完线程,需要把线程归还给线程池

      list.add(t);
      
      list.addLast(t);//若集合是LinkedList<Thread>,可以使用addLast方法添加到最后一个上
      

      线程池中元素添加删除是按队列先进先出特性实现的

    • 在JDK1.5之后,JDK内置了线程池,可以直接使用不用自己再用集合创建线程池了

    线程池的使用

    • java.util.concurrent.Executors:线程池的工厂类,用来生成线程池

    • Executors类的静态方法:

      static ExecutorService newFixedThreadPool(int nThreads)创建一个可重用固定线程数的线程池

      参数:int nThreads:创建线程池中包含线程的数量

      返回值:ExecutorService是一个接口类型,该方法返回的是ExecutorService接口的实现类对象,可以使用ExecutorService接口接收

    • java.util.concurrent.ExecutorService:线程池接口

      1. 用来从线程池中获取线程,调用start方法,执行线程任务

        submit(Runnable task) 提交一个Runnable任务用于执行

      2. 关闭/销毁线程的方法

        void shutdown()

    • 线程池的使用步骤:

      1. 使用线程池的工厂类Executors里面提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
      2. 创建一个类,实现Runnable接口,重写run方法,设置线程任务
      3. 调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
      4. 调用ExecutorService中的方法shutdown销毁线程池(不建议执行)
    • 示例Code

      public class ThreadPool {
          public static void main(String[] args) {
              ExecutorService es = Executors.newFixedThreadPool(5);
              Runnable runnable = new Runnable() {
                  @Override
                  public void run() {
                      System.out.println("current thread is : " + Thread.currentThread());
                  }
              };
              //线程池会一直开启,使用完了线程,会自动把线程归还给线程池,线程可以继续使用
              es.submit(runnable);//创建了一个新的线程执行
              es.submit(runnable);//创建了一个新的线程执行
              es.submit(runnable);//创建了一个新的线程执行
              es.submit(runnable);
              es.submit(runnable);
              es.submit(runnable);
              es.submit(runnable);
              
              //线程已经销毁的,就不能再submit开启一个线程了
              //es.shutdown();
              //es.submit(runnable);//RejectedExecutionException
          }
      }
      /*
      current thread is : Thread[pool-1-thread-3,5,main]
      current thread is : Thread[pool-1-thread-4,5,main]
      current thread is : Thread[pool-1-thread-5,5,main]
      current thread is : Thread[pool-1-thread-2,5,main]
      current thread is : Thread[pool-1-thread-1,5,main]
      current thread is : Thread[pool-1-thread-5,5,main]
      current thread is : Thread[pool-1-thread-3,5,main]
      */
      

    九、Lambda表达式

    函数式编程思想

    • 面向对象的思想:

      做一件事情,找一个能解决这个事情的对象,调用对象的方法,完成事情

    • 函数式编程思想:

      只要能获取到结果,谁去做,怎么做的都不重要,重视的是结果,不重视过程

    • Lambda表达式主要作用:

      重写接口中的方法(要求接口中有且仅有一个抽象方法才能用Lambda表达式)返回接口类型的对象,作用类似匿名内部类,不用再通过实现类创建接口类型的对象,写法比匿名内部类还简单,Lambda表达式还具有更简化的省略写法

    • 举例:

      实现类实现了Runnable,其中重写了run方法,将Runnable的实例化对象赋值给Thread构造方法再调用start开启线程。

      主要的目的是:将run方法体内的代码传递给Thread类

    • 示例Code

      public class LambdaExpression01 {
          public static void main(String[] args) {
      		//使用匿名内部类,实现多线程
              new Thread(new Runnable(){
                  @Override
                  public void run() {
                      System.out.println(Thread.currentThread().getName() + "new thread has been created");
                  }
              }).start();
      		//使用Lambda表达式,实现多线程
              //前面的小括号表示run方法的参数(无参数),代表不需要任何条件
              //中间的箭头代表将前面的参数传递给后面的代码
              new Thread(() -> {System.out.println(Thread.currentThread().getName() + "new thread has been created");}).start();
          }
      }
      

    Lambda表达式标准格式

    • 由三部分组成:

      1. 一些参数
      2. 一个箭头
      3. 一段代码
    • 格式:

      (参数列表) -> {一些重写方法的代码};

      解释说明格式:

      ():接口中抽象方法(相当于Runnable接口中的run方法)的参数列表,没有参数,就空着;有参数就写出参数,多个参数使用逗号分隔)

      ->:传递的意思,把参数传递给方法体{}

      {}:重写接口的抽象方法(相当于Runnable的实现类中重写run方法)的方法体

    • 示例Code

      public interface Cook {
          public abstract void makeFood();
      }
      
      //无参数无返回值的Lambda表达式,简化了匿名内部类的书写
      public class AppDemo01 {
          public static void main(String[] args) {
              invokeCook(new Cook(){
                  public void makeFood() {
                      System.out.println("makeFood method from Cook interface");
                  }
              });
      
              invokeCook(() -> {
                  System.out.println("makeFood method from Lambda Expression");
              });
          }
      
          public static void invokeCook(Cook cook) {
              cook.makeFood();
          }
      }
      /*
      makeFood method from Cook interface
      makeFode method from Lambda Expression
      */
      
    • 示例Code2

      //有参数有返回值的Lambda表达式
      public class AppDemo02 {
          public static void main(String[] args) {
              Person[] arr = new Person[] {
                new Person(45, "Nick"),
                new Person(12, "Jeff"),
                new Person(32, "Mohammed")
              };
      
              for(Person person : arr) {
                  System.out.println(person);
              }
      
              Arrays.sort(arr, new Comparator<Person>() {
                  @Override
                  public int compare(Person o1, Person o2) {
                      return o1.getAge() - o2.getAge();
                  }
              });
      
              Arrays.sort(arr, (Person o1, Person o2) -> {
                  return o1.getAge() - o2.getAge();
              });
      
              for(Person person : arr) {
                  System.out.println(person);
              }
          }
      }
      

    Lambda省略格式和Lambda使用前提

    • Lambda表达式:是可推导的,可以省略

      凡是根据上下文推导出来的内容,都可以省略书写

      可以省略的内容:

      1. (参数列表):括号中的参数列表的数据类型,可以省略不写

      2. (参数列表):括号中的参数若只有一个,类型和括号都可以省略(没有参数,就不能省略括号)

      3. {重写的方法体}:如果{}中的代码只有一行,无论是否有返回值都可以省略括号{}、return、分号;

        注意:要省略{},return, 分号必须一起省略

    • Lambda表达式使用的前提条件:

      1. 使用Lambda必须具有接口,而且要求接口中有且仅有一个抽象方法,若有两个抽象方法,编译器就不确定重写哪个方法了

        无论是JDK内置的Runnable、Comparator接口还是自定义接口,当接口中的抽象方法存在且唯一时,才可以使用Lambda

      2. 使用Lambda必须具有上下文推断

        也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例

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

    • 示例Code

      printAddResult((int num1, int num2) -> {
          return num1+num2; }, 15, 20);
      //Lambda省略格式
      printAddResult((num1, num2) -> 
                     num1+num2, 5, 6);
      
      Arrays.sort(arr, (Person o1, Person o2) -> {
          return o1.getAge() - o2.getAge();
      });
      //Lambda省略格式
      Arrays.sort(arr, (o1, o2) -> 
                  o1.getAge()-o2.getAge());
      
      invokeCook(() -> {
          System.out.println("makeFood method from Lambda Expression");
      });
      //Lambda省略格式
      invokeCook(() -> System.out.println("makeFood method from Lambda Expression"));
      

posted @   Ramentherapy  阅读(397)  评论(0编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示