1. 【前言、入门程序、常量、变量】

  • char 类型为 2 字节,可以存储一个汉字

2. 【数据类型转换、运算符、方法入门】

  • 范围大小:byte、short、char‐‐>int‐‐>long‐‐>float‐‐>double

  • byte 、short、char 变量运算时直接提升为int

  • float,double 使用科学计数法,所以字节少范围大

  • ascii: 0 对应 48、A 对应 65,a 对应 97

  • &&和 || 均为短路,前面计算出结合,后面不再进行

    public class Demo09Logic {
        public static void main(String[] args) {
            int a = 10;
            // false && ...
            System.out.println(3 > 4 && ++a < 100); // false
            System.out.println(a); // 10
            System.out.println("============");
    
            int b = 20;
            // true || ...
            System.out.println(3 < 4 || ++b < 100); // true
            System.out.println(b); // 20
        }
    }
    
  • && 和 & 的区别:

    • 都表示逻辑运算符 and 。
    • && 具有短路功能,& 不具有。
    • & 当两边的表达式不是boolean 类型的时候,可以用作位运算符。
  • 复合赋值运算符(+= 等)其中隐含了一个强制类型转换

    public class Demo07Operator {
        public static void main(String[] args) {
            byte num = 30;
            // num = num + 5;
            // num = byte + int
            // num = int + int
            // num = int
            // num = (byte) int
            num += 5;
            System.out.println(num); // 35
        }
    }
    
  • 对于 byte/short/char 三种类型来说,如果右侧赋值的数值没有超过范围,那么javac 编译器将会自动隐含地为我们补上一个(byte)(short)(char)。如果右侧超过了左侧范围,那么直接编译器报错。

    public class Demo12Notice {
        public static void main(String[] args) {
            // 右侧确实是一个int数字,但是没有超过左侧的范围,就是正确的。
            // int --> byte,不是自动类型转换
            byte num1 = /*(byte)*/ 30; // 右侧没有超过左侧的范围
            System.out.println(num1); // 30
    
            // byte num2 = 128; // 右侧超过了左侧的范围
    
            // int --> char,没有超过范围
            // 编译器将会自动补上一个隐含的(char)
            char zifu = /*(char)*/ 65;
            System.out.println(zifu); // A
        }
    }
    
  • 编译器的常量优化:在给变量进行赋值的时候,如果右侧的表达式当中全都是常量,没有任何变量,
    那么编译器javac 将会直接将若干个常量表达式计算得到结果。short result = 5 + 8; // 等号右边全都是常量,没有任何变量参与运算
    编译之后,得到的。class 字节码文件当中相当于【直接就是】:short result = 13;
    右侧的常量结果数值,没有超过左侧范围,所以正确。
    这称为“编译器的常量优化”。
    但是注意:一旦表达式当中有变量参与,那么就不能进行这种优化了。

    public class Demo13Notice {
        public static void main(String[] args) {
            short num1 = 10; // 正确写法,右侧没有超过左侧的范围,
    
            short a = 5;
            short b = 8;
            // short + short --> int + int --> int
            // short result = a + b; // 错误写法!左侧需要是int类型
    
            // 右侧不用变量,而是采用常量,而且只有两个常量,没有别人
            short result = 5 + 8;
            System.out.println(result);
    
            short result2 = 5 + a + 8; // 报错
        }
    }
    

3. 【流程控制语句】

  • switch 后面小括号当中只能是下列数据类型:
    基本数据类型:byte/short/char/int
    引用数据类型:Character、Byte、Short、Integer、String字符串、enum枚举

  • Java 中breakcontinue 后跟标签标签可以指定跳出或继续的循环:

    public class Main {
        public static void main(String[] args) {
            go:
            for (int i = 0; i < 10; i++) {
                for (int j = 0; j < 10; j++) {
                    System.out.println(j);
                    if (j == 1) break go;//0  1
                }
            }
        }
    }
    
  • 三目运算符:

    //三目运算符
    2>1 ? num1 : num2;//(错误的)
    //相当于
    if(2>1) {
        return num1;
    } else {
        return num2;
    }
    //三目运算符有返回值,应该写成
    int x = 2>1 ? num1 : num2;
    

5. 【数组】

  • Java 中数组也是一个对象,父类为 Object ,基本数据类型不是对象。

  • 动态初始化方法:int[] arr=new int[5]

  • 静态初始化:int[] arr=new int[]{1,2,3,4,5}

  • 静态初始化省略方式:int[] arr={1,2,3,4,5}

  • 动态初始化和静态初始化标准方式可以拆分为两个步骤,int[] arr; arr=new int[5];和arr=new int[]{1,2,3,4,5};,但是静态初始化的省略模式不可拆分为两个步骤。

    public class Main{
        public static void main(String[] args) {
            int[] arr = new int[1];
            System.out.println(arr);//[I@14ae5a5
            float[] arr2={1,2,3,4};
            System.out.println(arr2);//[F@7f31245a
        }
    }
    

    打印的为内存地址哈希值
    "["代表数组
    "I"代表 int
    "@"后面为 16 进制地址

  • 动态初始化数组时,int[] arr=new int[5]; 其中的元素会有一个默认值,规则如下:
    整数:0
    浮点数:0.0
    字符型:'\u0000' 非 null,非空格,看起来像空格,是个 unicode 字符
    布尔型:false
    引用类型:null

  • 静态初始化其实也有默认值的过程,只不过系统自动马上将默认值替换成为了大括号当中的具体数值。

  • Java 的内存划分,5 个部分

    1. 栈(Stack):存放的都是方法中的局部变量。方法的运行一定要在栈当中运行。
      局部变量:方法的参数,或者是方法{}内部的变量。
      作用域:一旦超出作用域,立刻从栈内存当中消失。
    2. 堆(Heap)凡是 new 出来的东西,都在堆当中。
      堆内存里面的东西都有一个地址值:16 进制
      堆内存里面的数据,都有默认值。规则:
      如果是整数 默认为 0
      如果是浮点数 默认为 0.0
      如果是字符 默认为'\u0000'
      如果是布尔 默认为 false
      如果是引用类型 默认为 null
    3. 方法区(Method Area):存储 class 相关信息,包含方法的信息。
    4. 本地方法栈(Native Method Stack):与操作系统相关。
    5. 寄存器(pc Register):与 CPU 相关。

6. 【类与对象、封装、构造方法】

  • 和当前包属于同一包,或者java.lang 包下,可以省略导包语句。
  • 成员变量会有默认初始值,和数组一样的规则。
  • 堆中 new 出来的类空间中的成员方法,在堆中保存的为成员方法在方法区中的内存地址。
  • 加载成员方法时,通过栈中的类变量的地址找到变量在堆中的空间,在通过成员方法在堆保存中的地址(指向方法区中的方法的地址)找到方法区中 class 文件的方法。
  • 对于基本类型中的boolean 值,Getter 方法为isXxx 的形式,Setter 方法为setXxx 形式。
  • 通过谁调用方法,this 就代表谁。
  • java.util.Random
    • int nextInt() 返回一个随机数,随机int 值(正负皆有可能)。
    • int nextInt(int n) 返回一个随机数,在 0(包括)和指定值(不包括)之间。
    • double nextDouble() 返回一个随机数,在 0.0 和 1.0 之间(很多位,均匀分布)。
    • import java.util.Random;
      Random r=new Random();
      //获取随机int数字,范围是所有int范围,有正负两种
      int num=r.nextInt();
      

8. 【String 类、static 关键字、Arrays 类、Math 类】

  • JDK8:String 底层是 char[]数组。;JDK9:String 底层是 byte[]字节数组。

  • String 的构造方法:

    • String(); 创建一个空白字符串,不含有任何内容。
    • String(char[] array); 根据字符数组的内容,创建对应字符串。
    • String(byte[] array); 根据字节数组的内容,来创建对应的字符串。
    • String(byte[] value, int offset, int count);
    • String(byte[] value, int offset, int count, String charset);
    • String(String original); 初始化一个新创建的 String 对象,使其表示一个与参数相同的字符序列。
    • String(StringBuffer buffer);
    • String(StringBuilder builder);
  • StringBuffer 线程安全,效率低;
    StringBuilder 线程不安全,效率高,JDK1.5 出现。

  • 从 JDK1.7 开始,字符串常量池在堆中,字符串直接写上的双引号字符串,就在字符串常量池中。

  • 字符串常量池中存储的字符串对象中存储的为实际数组的地址。

  • String 求长度为方法 length();

  • String 的 indexOf(String str); 找不到返回-1

  • String 常用方法

    • boolean equals (Object anObject)
    • boolean equalsIgnoreCase (String anotherString)
    • int length ()
    • String concat (String str)
    • char charAt (int index)
    • int indexOf (String str)
    • String substring (int beginIndex)
    • String substring (int beginIndex, int endIndex)
    • char[] toCharArray ()
    • byte[] getBytes ()
    • String replace (CharSequence target, CharSequence replacement) String 实现了CharSequence 接口,第 2 个参数替换第 1 个参数。
    • String[] split(String regex)
    • void getChars(int srcBegin,int srcEnd,char[] dst,int dstBegin) 将字符从此字符串复制到目标字符数组。
    • String trim() 返回字符串的副本,忽略前导空白和尾部空白。
    • compareTo(String anotherString);
    • boolean endsWith(String suffix);
    • boolean startsWith(String prefix);
    • boolean matches(String regex); 告知字符串是否匹配给定的正则表达式。
    • String toLowerCase();
    • String toUpperCase();
  • String 中 +concat() 方法对比:

    • concat() 方法,开辟一个新的数组,把原数组复制进去,并将要连接的数组,复制进新数组的后面。
    • + 号运算符,两个String 相 + 的源码为(new StringBuilder()).append(s1).append(s2).toString();

    concat() 效率比 + 高,只适用于 StringString 的拼接,而 + 适用于 String 与任何对象的拼接。

  • 正则中 \\.: 这要分两步看,首先字符串中的 \\ 被编译器解释为 \,然后作为正则表达式 \. 又被正则表达式引擎解释为 .,如果在字符串里只写 \. 的话,第一步就被直接解释为 .,之后作为正则表达式被解释时就变成匹配任意字符了。

  • public String[] split(String regex,int limit)
    limit 影响所得数组的长度。如果大于 0,数组的长度将不会大于 limit。如果 n 为非正,数组可以是任何长度。如果 n 为 0,结尾空字符串将被丢弃。

  • 对于本类当中的静态方法,调用时类名称可以省略。

  • 在方法区中存在一个静态区,存储静态的内容。

  • 静态代码块比构造方法先执行。

  • java.util.Arrays 数组工具类:

    • static String toString(数组); 将数组按照默认格式变成字符串,[元素 1,元素 2]

    • static void sort(T[] a, int fromIndex, int toIndex, Comparator<? super T> c): 根据指定比较器产生的顺序对指定对象数组的指定范围进行排序。

      • 重载方法数组类型可以改变(基本类型或引用类型)。
      • 重载方法fromIndextoIndex 可以省略。
      • 重载方法Comparator c 可以省略。
      • 排序比较时,a 是前面的数字,b 是后面的数字,返回 1 代表交换,返回 -1 代表不交换,返回 0 代表两个数一样,不一定交换不交换
    • static List<T> asList(T... a); 返回一个固定大小(无法添加元素)的 List<T> 集合。

    • static int binarySearch(Object[] a, int fromIndex, int toIndex, Object key);

      • 二分查找key,其中重载方法参数数组类型可以改变(基本类型或者引用类型),fromIndextoIndex 可以省略。
      • 数组必须是有序的。
    • static T[] copyOf(T[] original, int newLength): 复制指定的数组,截取或用 null 填充(如有必要),以使副本具有指定的长度

      • 重载方法数组类型可以改变(基本类型或引用类型),基本类型用 0 填充。
    • static T[] copyOfRange(T[] original, int from, int to): 将指定数组的指定范围复制到一个新数组。

      • 重载方法数组类型可以改变(基本类型或引用类型)。
    • static void fill(Object[] a, int fromIndex, int toIndex, Object val): 将指定的 Object 引用分配给指定 Object 数组指定范围中的每个元素。

      • 重载方法数组类型可以改变(基本类型或引用类型)。
      • 重载方法fromIndextoIndex 可以省略。
  • java.lang.Math :

    • public static double abs(double num):获取绝对值,重载方法返回值和参数类型一样。
    • public static double ceil(double num):向上取整
    • public static double floor(double num):向下取整
    • public static long round(double num):四舍五入
    • public static double sin(double x)
    • public static double asin(double x)
    • public static double cos(double x)
    • public static double acos(double x)
    • public static double tan(double x)
    • public static double atan(double x)
    • public static double log(double a)
    • public static double log10(double)
    • public static int max(int a, int b)
    • public static int min(int a, intb)
    • public static double pow(double a, double b)
  • double 可以进行 ++ 操作。

9. 【继承、super、this、抽象类】

  • 父类,也可以叫基类、超类。
  • 子类,也可以叫派生类。
  • 重写时子类方法的返回值必须等于父类的返回值或者是父类返回值的子类,权限必须大于等于父类的权限修饰符。
  • private 修饰的变量和方法不会被继承。
  • 继承时,子类和父类的方法必须是实例方法,如果父类和子类都是 static 方法,那么子类隐藏父类的方法,而不是重写父类方法。
  • 继承中,直接访问成员变量时,左边是谁就访问谁的变量,没有则向上找;通过成员方法访问成员变量时,方法属于谁,优先用谁,没有则向上找。
  • 继承时,会在子类的内存中开辟父类的内存,方法区加载的子类中会有父类 class 文件的地址。

10.【接口、多态】

  • Java7:接口可以包含

    1. 常量
    2. 抽象方法

    Java8,还可以包含

    1. 默认方法
    2. 静态方法

    Java9,还可以包含

    1. 私有方法
  • 接口中的方法默认为 public abstract,可以省略。

  • 接口中的默认方法用 default 修饰,默认且必须 public,实现接口不必须重写默认方法,也可以重写,可以通过实现类对象来直接调用默认方法。

  • 不能通过接口实现类的对象来调用接口当中的静态方法(继承可以)。必须通过接口名.方法名调用,因为可以实现多个接口,静态方法不一定是哪个接口中的。

  • 私有方法,解决多个默认方法之间重复代码问题
    格式:pritave 返回值类型 方法名称(参数列表){}
    private static 返回值类型 方法名称(参数列表){}

  • final 修饰的变量必须被赋值。

  • 接口中常量默认且必须为 public static final 修饰。

  • 接口是没有静态代码块或者构造方法的。

  • 问:怎么查看一个类实现的所有接口?
    答:①此类以及父类实现的接口都属于本类实现的接口。②此类所实现的接口继承的接口也属于本类所实现的接口。

  • 如果实现类实现的多个接口当中,存在重复的默认方法,那么实现类一定要对冲突的默认方法进行覆盖重写。

  • 一个类的父类方法和接口的默认方法冲突,优先用父类方法。

  • 接口与接口之间为多继承关系。

  • 在多态中,成员方法的访问规则是:看 new 的是谁,就优先用谁,没有则向上找。

  • 向上转型就是父类引用指向子类对象。

  • 对象名 instanceof 类名称,判断对象能不能当做后面类型的实例,结果为 boolean 值。

11.【final、权限、内部类、引用类型】

  • final 可以修饰类、方法、局部变量、成员变量。

  • final 用来修饰一个类的时候,不能有任何子类,可以有父类。

  • final 修饰一个方法时,此方法不能被覆盖重写。

  • publicprotected(default)private
    同一个类YESYESYESYES
    同一个包YESYESYESNO
    不同包子类YESYESNONO
    不同包非子类YESNONONO
  • protected 修饰的字段或方法在不同包的子类中允许用子类对象来访问,但不允许用父类对象来访问。

    package package1;
    
    public class Fu {
        protected String num = "fu";
    }
    
    package package2;
    
    import package1.Fu;
    
    public class Zi extends Fu{
        public void method(){
            //正确写法
            Zi zi = new Zi();
            System.out.println(zi.num);
    
            //错误写法,无法访问
            //Fu fu = new Fu();
            //System.out.println(fu.num);
            //
            //Fu zi1 = new Zi();
            //System.out.println(zi1.num);
        }
    }
    
  • 内部类分为成员内部类和局部内部类,定义格式如下:

    public class Test {
        public class Inner{
    
        }
    }
    

    注意:内用外,随意访问,外用内,需要内部类对象。

  • 内部类的编译后 class 文件命名为外部类名$内部类名.class,例如 Test$Inner.class

  • 内部类创建对象方法为 Test.Inner inner = new Test().new Inner();

  • 如果出现了变量名重复,内部类访问外部类变量使用 外部类名.this.变量名

    public class Outer {
        int num = 10; //外部类成员变量
        public class Inner{
            int num = 20; //内部类成员变量
            public void method(){
                int num = 30;//布局变量
                System.out.println(num);//30
                System.out.println(this.num);//20
                System.out.println(Outer.this.num);//10
            }
        }
    }
    
  • 局部内部类:定义在一个方法内部,只有此方法可以使用本类。

    public class Outer {
        public void methodOuter(){
            class Inner{
                int num=10;
                public void methodInner(){
                    System.out.println(num);
                }
            }
    
            Inner inner = new Inner();
            inner.methodInner();
        }
    }
    
  • 局部内部类编译后的 class 文件为 外部类名$1内部类名.class

  • 匿名内部类编译后的文件名为类名 $1.class。

  • 定义一个类的时候,权限修饰符规则:

    1. 外部类:public/(default)
    2. 成员内部类:public/protected/(default)/private
    3. 局部内部类:什么都不写(局部的东西只能在此方法使用,没有权限修饰符)
  • 为什么外部类不能使用 protected 修饰?
    protected 只比(default)多了一个不同包的子类访问权限,当外部类使用 protected 修饰的时候,因为不同包,又非子类,所以无法导入此类,protected 修饰的外部类也就没有了子类,就不需要使用 protected 了,所以外部类只能使用 public 和(default)修饰。

  • 内部类使用 private 修饰时,只有此内部类所在的外部类可以使用。

  • 留疑:protected 修饰内部类时,在不同包的子类中怎么访问内部类以及内部类的继承。

    package package1;
    
    public class Fu {
        protected class Inner {
            public int num = 10;
        }
    }
    
    package package2;
    
    import package1.Fu;
    
    public class Zi extends Fu {
        public static void main(String[] args) {
            //new Zi().new Inner();此行报错,无法访问
        }
    }
    

    答:new 是调用内部类的构造函数,因为内部类为 protected,所以内部类的构造函数为 protected 修饰,当内部内的构造函数为 protected 修饰的时候,这个构造函数只能由这个内部类的子类调用,Zi 类是 Fu 的子类,并非内部类的子类,所以无法调用内部类的构造函数。

    package package1;
    
    public class Fu {
        protected class Inner {
            public int num = 10;
    
            public Inner() {
            }
        }
    }
    
    package package2;
    
    import package1.Fu;
    
    public class Zi extends Fu {
        public static void main(String[] args) {
            new Zi().new Inner();
        }
    }
    

    此时,只需要将内部类的构造函数的权限改为 public,即可在外包中调用此构造函数。此时外包中 Fu 的子类既可以访问 Inner 类(因为 protected),也可以访问内部类的构造方法(由于 public 修饰),所以可以成功构建内部类对象。

  • 局部内部类访问方法的局部变量时,这个局部变量必须是【有效 final 的】。
    备注:从 JDK8+ 开始,只要局部变量事实不变,那么 final 关键字可以省略。
    原因:

    1. new 出来的对象在堆内存当中。
    2. 局部变量是跟着方法走的,在栈内存当中。
    3. 方法运行结束之后,立刻出栈,局部变量就会立刻消失。
    4. 但是 new 出来的对象会在堆当中持续存在,直到垃圾回收消失。
  • 匿名内部类定义格式

    package package1;
    
    public class Main {
        MyInterface t = new MyInterface() {
            @Override
            public void method() {
                System.out.println("匿名内部类实现的方法");
            }
        };
    }
    

12.【Object 类、常用 API】

  • 重写 equals 时的写法

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        //age为int,name为string
        return age == student.age &&
                Objects.equals(name, student.name);
    }
    
  • Objects.equals(Object a, Object b) 源码

    public static boolean equals(Object a, Object b) {
        return (a == b) || (a != null && a.equals(b));
    }
    
  • java.lang.Object:

    • Object clone() 创建并返回此对象的一个副本。
  • 中国会把时间增加 8h,即为到 08:00:00 的时间。

  • Date();//获取当前系统的时间和日期,如 Sat Jan 18 11:49:45 CST 2020

  • Date(long date);//将毫秒转为对应日期

    package package1;
    
    import java.util.Date;
    
    public class Main {
        public static void main(String[] args) {
            Date date = new Date(0L);
            System.out.println(date);//Thu Jan 01 08:00:00 CST 1970
        }
    }
    
  • long getTime();//将 Date 对象转换为毫秒

  • DateFormat位于java.text.DateFormat 对日期进行格式化,或者将文本解析为日期。

  • DateFormat 成员方法:

    • String format(Date date) 按照指定的模式,把 Date 日期,格式化为符合模式的字符串。
    • Date parse(String source) 把符合模式的字符串,解析为 Date 日期。
  • DateFormat 为抽象类,使用子类 SimpleDateFormat

  • SimpleDateFormat(String pattern); 构造函数,参数为给定 的模式,y-年,M-月,d-日,H-时,m-分,s-秒。

    package package1;
    
    import java.text.SimpleDateFormat;
    import java.util.Date;
    
    public class Main {
        public static void main(String[] args) {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH时:mm分:ss秒");
            Date date = new Date();
            String d = sdf.format(date);
            System.out.println(date);//Sun Jan 26 14:17:30 CST 2020
            System.out.println(d);//2020年01月26日 14时:17分:30秒
        }
    }
    
  • parse 方法

    package package1;
    
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    
    public class Main {
        public static void main(String[] args) throws ParseException {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH时:mm分:ss秒");
            Date date = sdf.parse("2088年08月08日 15时:51分:54秒");
            System.out.println(date);
        }
    }
    

    会抛出 ParseException 异常。

  • java.util.Calendar 日历类,提供了很多操作日历字段的方法(YEAR,MONTH,DAY_OF_MONTH,HOUR)。

  • Calendar 类为抽象类,无法创建对象,使用静态方法 static Calendar getInstance() 返回一个子类对象。

    package package1;
    
    import java.util.Calendar;
    
    public class Main {
        public static void main(String[] args) {
            Calendar calendar = Calendar.getInstance();
            System.out.println(calendar);
        }
    }
    //java.util.GregorianCalendar[time=1580021525270,areFieldsSet=true,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="Asia/Shanghai",offset=28800000,dstSavings=0,useDaylight=false,transitions=19,lastRule=null],firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YEAR=2020,MONTH=0,WEEK_OF_YEAR=5,WEEK_OF_MONTH=5,DAY_OF_MONTH=26,DAY_OF_YEAR=26,DAY_OF_WEEK=1,DAY_OF_WEEK_IN_MONTH=4,AM_PM=1,HOUR=2,HOUR_OF_DAY=14,MINUTE=52,SECOND=5,MILLISECOND=270,ZONE_OFFSET=28800000,DST_OFFSET=0]
    
  • Calendar 类常用的成员方法

    • public int get(int field); 返回给定日历字段的值。
    • public void set(int field,int value); 给指定的日历字段设置为指定的值。
    • public final void set(int year, int month, int date); 给日历设置年月日。
    • public abstract void add(int field,int amount); 给指定字段添加或者减去指定的时间量。
    • public Date getTime(); 返回一个 Calendar 时间值的 Date 对象。
  • Calendar 成员方法中的 int field:日历类的字段,可以使用 Calendar 类的静态成员变量获取。例如

    public static final int YEAR = 1;
    public static final int MONTH = 2;
    public static final int DATE = 5;
    public static final int DAY_OF_MONTH = 5;
    public static final int HOUR = 10;
    public static final int MINUTE = 12;
    public static final int SECOND = 13;
    
  • 西方的月份为 0-11。

  • java.lang.System 中常用静态方法:

    • public static long currentTimeMillis(); 返回以毫秒为单位的当前时间。
    • public static void arraycopy(Object src,int srcPos,Object dest,int destPos,int length);src 为数组源,srcPos 源数组中起始位置,dest 目标数组,destPos 目标数组中起始位置,length 长度,将数组中指定数据拷贝到另一个数组中。
  • String 是被 final 修饰的数组,StringBuilder 没有被 final 修饰,可以改变长短,在内存中始终是一个数组,会自动扩容。

  • StringBuilder 扩容:每次增加一倍并 +2,若还是不够,直接扩大到所需长度。

  • StringBuilder 构造方法:

    • StringBuilder(); 构造一个不带任何字符的字符串生成器,初始容量为 16 个字符。
    • StringBuilder(String str); 构造一个字符串生成器,并初始化为指定的字符串内容。
  • StringBuilder 常用的方法:

    • public StringBuilder append(): 添加任意类型数据的字符串形式,并返回当前对象自身。
    • public String toString(); 将当前StringBuilder 对象转换为 String 对象。
    • public StringBuilder delete(int start, int end); 返回值为此对象,没必要接收。
    • public StringBuilder deleteCharAt(int index); 返回值为此对象,没必要接收。
    • public StringBuilder insert(int offset, Object obj); 将参数的字符串表示形式插入此字符序列中。
      • 第二个参数可以是任意类型,也可以为数组。
      • 第二个参数为数组的话,可以增加第三个和第四个参数为数组的开始结束位置。
    • public void setCharAt(int index, char ch);
    • public StringBuilder reverse(); 反转,字符串可以不接收。
    • StringBuilder replace(int start, int end, String str); 使用给定 String 中的字符替换此序列的子字符串中的字符。
  • StringStringBuilder 的相互转换:

    • StringStringBuilder:使用StringBuilder 的构造方法StringBuilder(String);
    • StringBuilderString:使用StringBuildertoString() 方法。
  • Integerint 相互转换:

    • 装箱(基本数据类型 →包装类)

      • 构造方法:

        • Integer(int value);
        • Integer(String s); 不是基本类型的字符串会抛异常
      • 静态方法:

        • static Integer valueOf(int i);
        • static Integer valueOf(String s);
    • 拆箱(包装类 →基本数据类型)

      • 成员方法int intValue(); 以 int 类型返回该 Integer 的值。
  • 自动装箱与自动拆箱

    import java.util.ArrayList;
    
    /*
        自动装箱与自动拆箱:基本类型的数据和包装类之间可以自动的相互转换
        JDK1.5之后出现的新特性
     */
    public class Demo02Ineger {
        public static void main(String[] args) {
            /*
                自动装箱:直接把int类型的整数赋值包装类
                Integer in = 1; 就相当于 Integer in = new Integer(1);
             */
            Integer in = 1;
    
            /*
                自动拆箱:in是包装类,无法直接参与运算,可以自动转换为基本数据类型,在进行计算
                in+2;就相当于 in.intValue() + 2 = 3
                in = in.intVale() + 2 = 3 又是一个自动装箱
             */
            in = in + 2;
    
            ArrayList<Integer> list = new ArrayList<>();
            /*
                ArrayList集合无法直接存储整数,可以存储Integer包装类
             */
            list.add(1); //-->自动装箱 list.add(new Integer(1));
    
            int a = list.get(0); //-->自动拆箱  list.get(0).intValue();
        }
    }
    
  • 基本类型与字符串类型之间的相互转换:

    • 基本类型 →字符串(String)

      1. 基本类型的值 +"" 最简单的方法(工作中常用)
      2. 包装类的静态方法toString(参数)static String toString(int i) 返回一个表示指定整数的 String 对象。
      3. String 类的静态方法valueOf(参数)static String valueOf(int i) 返回int 参数的字符串表示形式。
    • 字符串(String)→基本类型

      • 使用包装类的静态方法parseXXX("字符串");
      • 使用包装类的valueOf(String s);
      • Integer 类:static int parseInt(String s)
      • Double 类:static double parseDouble(String s)
  • Integer 中有关进制的方法:

    • static Integer valueOf(String s, int radix)
    • static String toBinaryString(int i)
    • static String toOctalString(int i)
    • static String toHexString(int i)

13.【Collection、泛型】

  • 集合

  • Collection 接口中方法:

    • public boolean add(E e): 把给定的对象添加到当前集合中 。
    • public void clear() :清空集合中所有的元素。
    • public boolean remove(E e): 把给定的对象在当前集合中删除,只删除第一个。
    • public boolean contains(E e): 判断当前集合中是否包含给定的对象。
    • public boolean isEmpty(): 判断当前集合是否为空。
    • public int size(): 返回集合中元素的个数。
    • public Object[] toArray(): 把集合中的元素,存储到数组中。
    • public T[] toArray(T[] a):返回数组,返回数组的运行时类型与指定数组的运行时类型相同,如果传入的数组长度能放下,就放到传入的数组中,否则返回一个新数组。

    不带参数的 toArray() 方法返回值为 Object[] 类型,无法强转成别的类型,运行会报错,这时候使用第二个方法返回指定类型,例如 Integer[] nums = list.toArray(new Integer[list.size()]);
    为什么不能使用方法引用 toArray(Integer[]::new) , 因为参数是一个数组,而不是一个函数式接口,不能使用方法引用。

  • java.util.Iterator 接口:迭代器(对集合进行遍历):

    • boolean hasNext() 如果仍有元素可以迭代,则返回 true。
    • E next() 返回迭代的下一个元素。
  • Iterator 迭代器,是一个接口,实现类的获取方式为使用Collection接口的 Iterator<E> iterator() 方法。

  • 增强 for 循环,底层使用的是迭代器,JDK1.5 以后的新特性。
    Collection<E> extends Iterable<E>:所有的单列集合都可以使用增强 for,
    public interface Iterable<T> 实现这个接口允许对象成为 foreach 语句的目标。

  • 增强 for 循环格式:

    for(集合/数组的数据类型 变量名:集合名/数组名){
        sout(变量名);
    }
    
  • 泛型中:(约定俗称)

    • E - Element (在集合中使用,因为集合中存放的是元素)
    • T - Type(Java 类)
    • K - Key(键)
    • V - Value(值)
    • N - Number(数值类型)
    • ? - 表示不确定的 Java 类型
  • 创建对象的时候,会把数据类型作为参数传递,赋值为泛型。

  • 使用泛型的时候,泛型只是一个类型,无法使用泛型中的方法,变量等,若对泛型进行了限定,例如 <T extends Comparable<T>>,则可以使用 compareTo(T e) 方法。

  • ArrayList list = new ArrayList(); 默认为 Object 类型。

  • JDK1.7 之前,创建集合前后泛型都需要写上。

  • 泛型的定义

    public class GenericClass<E> {
        private E name;
    }
    
  • 含有泛型的方法

    public class GenericClass {
        public <M> void method1(M m) {
        }
    
        public static <S> void method2(S m) {
        }
    }
    
  • 含有泛型的接口,第一种使用方式:定义接口的实现类,实现接口,指定接口的泛型,例如

    public interface Iterator<E> {
        E next();
    }
    
    public final class Scanner implements Iterator<String> {
        public String next() {
        }
    }
    
  • 含有泛型的接口第二种使用方式:接口使用什么泛型,实现类就使用什么泛型,类跟着接口走

    public class ArrayList<E> implements List<E> {
        public boolean add(E e) {
        }
    
        public E get(int index) {
        }
    }
    
  • 泛型没有继承概念的,此处不能使用 Object,Object 代表确定的类型,不能传入别的类型,此处也不能用 E(需要在方法声明泛型),list 为对象,需要确定 泛型类型,所以使用?,可以接受任何泛型。

    package package1;
    
    import java.util.ArrayList;
    import java.util.Iterator;
    
    public class Demo05Generic {
        public static void main(String[] args) {
            ArrayList<Integer> list01 = new ArrayList<>();
            list01.add(1);
            list01.add(2);
    
            ArrayList<String> list02 = new ArrayList<>();
            list02.add("a");
            list02.add("b");
    
            printArray(list01);
            printArray(list02);
    
            //ArrayList<?> list03 = new ArrayList<?>();
        }
    
        /*
            定义一个方法,能遍历所有类型的ArrayList集合
            这时候我们不知道ArrayList集合使用什么数据类型,可以泛型的通配符?来接收数据类型
            注意:
                泛型没有继承概念的
         */
        public static void printArray(ArrayList<?> list) {
            //使用迭代器遍历集合
            Iterator<?> it = list.iterator();
            while (it.hasNext()) {
                //it.next()方法,取出的元素是Object,可以接收任意的数据类型
                Object o = it.next();
                System.out.println(o);
            }
        }
    }
    
  • 泛型的限定

    package package1;
    
    import java.util.ArrayList;
    import java.util.Collection;
    
    /*
        泛型的上限限定: ? extends E  代表使用的泛型只能是E类型的子类/本身
        泛型的下限限定: ? super E    代表使用的泛型只能是E类型的父类/本身
     */
    public class Demo05Generic {
        public static void main(String[] args) {
            Collection<Integer> list1 = new ArrayList<Integer>();
            Collection<String> list2 = new ArrayList<String>();
            Collection<Number> list3 = new ArrayList<Number>();
            Collection<Object> list4 = new ArrayList<Object>();
    
            getElement1(list1);
            //getElement1(list2);//报错
            getElement1(list3);
            //getElement1(list4);//报错
    
            //getElement2(list1);//报错
            //getElement2(list2);//报错
            getElement2(list3);
            getElement2(list4);
    
            /*
                类与类之间的继承关系
                Integer extends Number extends Object
                String extends Object
             */
    
        }
    
        // 泛型的上限:此时的泛型?,必须是Number类型或者Number类型的子类
        public static void getElement1(Collection<? extends Number> coll) {
        }
    
        // 泛型的下限:此时的泛型?,必须是Number类型或者Number类型的父类
        public static void getElement2(Collection<? super Number> coll) {
        }
    }
    
  • 在泛型的限定中,<? extends E> 中,?不一定必须是 E 的子类,也可以实现 E 接口,与多态一样。

  • 对泛型进行多个限定的时候可以使用 & 符号。比如,<T extends Object & Comparable<? super T>> ,需要注意的是,当限定中既有接口又有类的时候,需要把类放置在 & 的前面,并且只能限定一个类(单继承),可以限定多个接口。

14.【List、Set】

  • Collections.shuffle(List<?> list); 使用随机源打乱列表顺序。

  • 平衡树,即平衡二叉树,它是一棵空树或它的左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树。

  • 红黑树从根节点到叶子节点的最长路径不大于最短路径的 2 倍。

  • 红黑树的约束:

    1. 节点可以是红色或者黑色的。
    2. 根节点是黑色的。
    3. 叶子节点(空节点)是黑色的。
    4. 每个红色的结点的子节点都是黑色的。
    5. 任何一个结点到其每一个叶子节点的任何路径上黑色节点数相同。
  • List 接口中带索引的方法:

    • public void add(int index, E element): 将指定的元素,添加到该集合中的指定位置上。
    • public E set(int index, E element):用指定元素替换集合中指定位置的元素,返回值的更新前的元素。
    • void remove(int position) :从此滚动列表中移除指定位置处的项。
  • ArrayList 底层是数组,查询快,增删慢,当第一次调用 add 添加元素时 ArrayList 才会设置数组默认长度 10,之后超出时会自动扩容,是以前的 1.5 倍,每次扩容会复制以前的数组到新的数组。

  • Arraylist 实现不是同步的,即线程不安全。

  • LinkedList 底层是一个双向链表:

    • public void addFirst(E e):将指定元素插入此列表的开头。
    • public void addLast(E e):将指定元素添加到此列表的结尾。
    • public void push(E e):与addFirst 等效。
    • public E getFirst():返回此列表的第一个元素。
    • public E getLast():返回此列表的最后一个元素。
    • public E removeFirst():移除并返回此列表的第一个元素。
    • public E removeLast():移除并返回此列表的最后一个元素。
    • public E pop():与removeFirst 等效。
  • Vector<E> 是同步的,线程安全,是同步的,特有方法(已过时):

    • void addElement(E obj);
    • Enumeration<E> elements(); 返回此向量的组件的枚举。
  • Enumeration<E> 方法:

    • boolean hasMoreElements(); 测试此枚举是否包含更多的元素。
    • E nextElement(); 如果此枚举对象至少还有一个可提供的元素,则返回此枚举的下一个元素。
  • Set<E> 接口不允许存储重复的元素。

  • HashSet<E> 是一个无序集合,底层是一个哈希表结构(实际上是一个 HashMap 实例),查询速度很快。

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

  • Object 类中 public native int hashCode() 方法返回对象的哈希码值。

  • native代表该方法调用的是本地操作系统的方法。

  • Object.toString() 方法源码:
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
    调用了哈希码的十六进制形式。

  • 不重写 hashCode() 的时候,是根据在内存中的真实地址去计算哈希码。

  • equal() 相等的两个对象他们的 hashCode() 肯定相等,hashCode() 相等的两个对象他们的 equal() 不一定相等。

  • 不同对象的哈希码有可能相同,hashCode() 不绝对可靠,但是效率高,所以先判断 hashCode() 是否相同,若相同,再去判断 equals() 可以提高程序的效率。

  • package package1;
    
    public class Demo01HashCode {
        public static void main(String[] args) {
            //Person类继承了Object类,所以可以使用Object类的hashCode方法
            Person p1 = new Person();
            int h1 = p1.hashCode();
            System.out.println(h1);//1967205423
    
            Person p2 = new Person();
            int h2 = p2.hashCode();
            System.out.println(h2);//42121758
    
            /*
                toString方法的源码:
                    return getClass().getName() + "@" + Integer.toHexString(hashCode());
             */
            System.out.println(p1);//com.itheima.demo03.hashCode.Person@75412c2f
            System.out.println(p2);//com.itheima.demo03.hashCode.Person@282ba1e
            System.out.println(p1 == p2);//false
    
            /*
                String类的哈希值
                    String类重写Obejct类的hashCode方法
             */
            String s1 = new String("abc");
            String s2 = new String("abc");
            System.out.println(s1.hashCode());//96354
            System.out.println(s2.hashCode());//96354
            System.out.println(s1.equals(s2));//true
            System.out.println(s1==s2);//false
    
            System.out.println("重地".hashCode());//1179395
            System.out.println("通话".hashCode());//1179395
        }
    }
    
  • 哈希表的初始容量为 16,特点为查询速度快。

  • jdk1.7:哈希表由数组 + 链表构成
    jdk1.8:哈希表由数组 + 链表 + 红黑树(提高查询的速度)构成

    当链表中的元素超过了 8 个以后,链表就会转换为红黑树。

  • Set 集合在调用 add 方法的时候,add 方法会调用元素的 hashCode()equals() 方法判断元素是否重复,首先会调用元素的 hashCode() 方法,寻找这个哈希值有没有对应元素,若没有,存入,若对应哈希值有元素(哈希冲突),就会调用 equals() 方法和哈希值相同的元素就行比较,若不同则存入,相同则不存。

  • java.util.LinkedHashSet 集合继承 HashSet,底层是一个哈希表(数组 + 链表\红黑树)+ 链表:多了一条链表记录元素的存储顺序,保证元素有序。

  • TreeSet 集合中的数据根据数据大小进行排序,可以自然排序,也可以自定义排序顺序。

  • 可变参数是 JDK1.5 之后出现的新特性,可变参数底层就是一个数组,根据传递参数个数不同,会创建不同长度的数组,来存储这些参数,格式:

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

    注意:
    1.一个方法的参数列表,只能有一个可变参数
    2.如果方法的参数有多个,那么可变参数必须写在参数列表的末尾

package package1;
  
public class Main {
      public static void main(String[] args) {
          int ans=add(1,2,3);
          System.out.println(ans);//6
      }
  
    public static int add(int... arr) {
          int sum = 0;
          for (int i : arr) {
              sum += i;
          }
          return sum;
      }
  }
  • java.utils.Collections 常用方法:

    • static <T> boolean addAll(Collection<T> c, T... elements):往集合中添加一些元素。
    • static void shuffle(List<?> list):打乱顺序:打乱集合顺序。
    • static <T> void sort(List<T> list):将集合中元素按照默认规则排序。
    • static <T> void sort(List<T> list,Comparator<? super T> ):将集合中元素按照指定规则排序。
    • static <T> int binarySearch(List<? extends T> list, T key ,Comparator<? super T> c); 查询不到的时候返回值 为-(插入点)-1,插入点为第一个比此点大的点,有序指的是按照 Comparator 的顺序。
    • static <T> int binarySearch(List<? extends Comparable<? extends T>> list, T key);
    • static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll);
    • static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp);
    • static <T> boolean replaceAll(List<T> list, T oldVal, T newVal);
    • static void reverse(List<?> list);
    • static void swap(List<?> list, int i, int j);

    sort(List list)使用前提:被排序的集合里边存储的元素,必须实现 Comparable,重写接口中的方法 compareTo 定义排序的规则。
    Comparable 接口的排序规则:自己(this)-参数:升序。

  • Comparator 和 Comparable 的区别:
    Comparable:自己(this)和别人(参数)比较,自己需要实现 Comparable 接口,重写比较的规则 compareTo 方法。
    Comparator:相当于找一个第三方的裁判,比较两个元素。
    Comparator 的排序规则:o1-o2:升序。

    package package1;
    
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.Comparator;
    import java.util.List;
    
    public class Main {
        public static void main(String[] args) {
            List<Integer> list = new ArrayList<>();
            Collections.addAll(list, 1, 2, 3);
            Collections.sort(list, new Comparator<Integer>() {
                @Override
                public int compare(Integer o1, Integer o2) {
                    return o1 - o2;
                }
            });
        }
    }
    

15.【Map】

  • HashMap<K,V> 基于哈希表的 Map 接口的实现,无序,HashSet 实际上是一个 HashMap 实例,HashSet 构造源码:

    public HashSet() {
        map = new HashMap<>();
    }
    
  • HashMap 实现是不同步的,线程不安全集合,是多线程的集合,速度快。

  • LinkedHashMap<K,V>:哈希表和链表实现,保证有序,key 按存入顺序排序,但 key 对应的 value 会被后来者更新。

  • HashMap<K,V> 常用方法:

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

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

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

    • public V remove(Object key): 把指定的键 所对应的键值对元素 在 Map 集合中删除,返回被删除元素的。
    • public V get(Object key): 根据指定的键,在 Map 集合中获取对应的值。
    • boolean containsKey(Object key): 判断集合中是否包含指定的键。
    • boolean containsValue(Object value): 判断集合中是否包含指定的值。
    • void putAll(Map<? extends K,? extends V> m):将指定映射的所有映射关系复制到此映射中,有重复则替换当前映射。
    • Collection<V> values():返回此映射所包含的值的Collection 视图。
    • Set<K> keySet() :返回此映射中包含的键的 Set 视图。
    • Set<Map.Entry<K,V>> entrySet(): 返回此映射中包含的映射关系的 Set 视图。

    遍历 map 集合的时候可以使用 keySetentrySet 方法,不能使用迭代器,iterator 方法为 Collection 接口中的方法,Map 接口中没有此方法。

  • Map.Entry<K,V>:在 Map 接口中有一个内部静态接口 Entry,其对象用来记录一个键值对关系。常用方法:

    • K getKey():返回与此项对应的键。
    • K getValue():返回与此项对应的值。
  • Map 集合中,作为 key 的元素,必须重写 hashCode 方法和 equals 方法,以保证 key 唯一。

  • Hashtable 集合的键和值都不能为 null,别的集合可以。

  • Hashtable 集合底层是哈希表,实现了 Map<K,V> 接口,是同步的,线程安全,单线程集合,速度慢。

  • HashtableVector 集合一样,在 jdk1.2 版本之后被更先进的集合(HashMap,ArrayList)取代了。

  • Hashtable 的子类 Properties 依然活跃在历史舞台,Properties 集合是一个唯一和 IO 流相结合的集合。

  • 了解:JDK9 新特性

    package package1;
    
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    
    /*
        JDK9的新特性:
            List接口,Set接口,Map接口:里边增加了一个静态的方法of,可以给集合一次性添加多个元素
            static <E> List<E> of(E... elements)
            使用前提:
                当集合中存储的元素的个数已经确定了,不在改变时使用
         注意:
            1.of方法只适用于List接口,Set接口,Map接口,不适用于接接口的实现类
            2.of方法的返回值是一个不能改变的集合,集合不能再使用add,put方法添加元素,会抛出异常
            3.Set接口和Map接口在调用of方法的时候,不能有重复的元素,否则会抛出异常
     */
    public class Demo01JDK9 {
        public static void main(String[] args) {
            List<String> list = List.of("a", "b", "a", "c", "d");
            System.out.println(list);//[a, b, a, c, d]
            //list.add("w");//UnsupportedOperationException:不支持操作异常
    
            //Set<String> set = Set.of("a", "b", "a", "c", "d");//IllegalArgumentException:非法参数异常,有重复的元素
            Set<String> set = Set.of("a", "b", "c", "d");
            System.out.println(set);
            //set.add("w");//UnsupportedOperationException:不支持操作异常
    
            //Map<String, Integer> map = Map.of("张三", 18, "李四", 19, "王五", 20,"张三",19);////IllegalArgumentException:非法参数异常,有重复的元素
            Map<String, Integer> map = Map.of("张三", 18, "李四", 19, "王五", 20);
            System.out.println(map);//{王五=20, 李四=19, 张三=18}
            //map.put("赵四",30);//UnsupportedOperationException:不支持操作异常
        }
    }
    

16.【异常、线程】

  • 异常的根类是 java.lang.Throwable,其下有两个子类:java.lang.Errorjava.lang.Exception

  • Error 指不应该试图捕获的严重问题,比如创建数组太大。

  • Exception 分为编译期异常(cheked 异常),进行编译(写代码)时 Java 程序出现的问题和 RuntimeException

  • RuntimeExceptionException 的子类,指可能在 Java 虚拟机正常运行期间抛出的异常的超类。

    • Exception 的编译期异常,必须处理,要么throws,要么try...catch
    • RuntimeException 或者RuntimeException 的子类对象为运行期异常,我们可以不处理,默认交给 JVM 处理(打印异常对象,中断程序)。
  • Throwable 中的常用方法:

    • public void printStackTrace():打印异常的详细信息,包含了异常的类型、异常的原因、异常出现的位置,在开发和调试阶段都使用printStackTrace
    • public String getMessage():获取发生异常的原因,简短信息,例如:throw 异常时输入的 message。
    • public String toString():返回详细消息的字符串,例如:异常类型 +message。
  • 虚拟机处理异常的方式为中断处理。

  • 异常产生的原理:

  • throw 关键字:可以使用 throw 关键字在指定的方法中抛出指定的异常,格式 throw new xxxException("异常产生的原因");
    注意:

    1. throw 关键字必须写在方法的内部。
    2. throw 关键字后边 new 的对象必须是Exception 或者Exception 的子类对象。
    3. throw 关键字抛出指定的异常对象,我们就必须处理这个异常对象。
      • throw 关键字后边创建的是RuntimeException 或者是RuntimeException 的子类对象,我们可以不处理,默认交给 JVM 处理(打印异常对象,中断程序)。
      • throw 关键字后边创建的是编译异常(写代码的时候报错),我们就必须处理这个异常,要么throws,要么try...catch
  • Objects 中的非空判断方法:public static <T> T requireNonNull(T obj),源码:

    public static <T> T requireNonNull(T obj) {
        if (obj == null)
            throw new NullPointerException();
        return obj;
    }
    

    可以简化书写,其重载为双参数:public static <T> T requireNonNull(T obj,String message);

  • finallycatch 后面代码的区别:处理异常的时候,在 catch 中有可能出现跳转,这时候,catch 后面的代码就不会执行,但是 finally 中的代码一定会执行。

  • finally 中若有 return,则一定会返回此值,所以要避免 finally 中写 return

  • 如果父类抛出了多个异常,子类重写父类方法时,抛出和父类相同的异常或者是父类异常的子类或者不抛出异常。

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

  • 自定义异常类:

    • 自定义异常类一般都是以Exception 结尾,说明该类是一个异常类。
    • 自定义异常类,必须的继承Exception 或者RuntimeException

    格式:

    /*
        自定义异常类:
            java提供的异常类,不够我们使用,需要自己定义一些异常类
        格式:
            public class XXXExcepiton extends Exception | RuntimeException{
                添加一个空参数的构造方法
                添加一个带异常信息的构造方法
            }
     */
    public class RegisterException extends /*Exception*/ RuntimeException {
        //添加一个空参数的构造方法
        public RegisterException() { super(); }
    
        /*
            添加一个带异常信息的构造方法
            查看源码发现,所有的异常类都会有一个带异常信息的构造方法,方法内部会调用父类带异常信息的构造方法,让父类来处理这个异常信息
         */
        public RegisterException(String message) { super(message); }
    }
    
  • 并发:两个或多个事件在同一时间段发生(交替执行)。

  • 并行:两个或多个事件在同一时刻发生(同时发生)。

  • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

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

  • 线程调度:

    • 分时调度:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
    • 抢占式调度:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java 使用的为抢占式调度。
  • 多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让 CPU 的使用率更高。

  • main 线程:

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

    1. 创建一个类继承Thread 类。
    2. 在 Thread 类的子类中重写 Thread 类中的 run 方法,设置线程任务。
    3. 创建 Thread 类的子类对象。
    4. 调用 Thread 类中的方法 start 方法,开启新的线程,执行 run 方法。
    package package1;
    
    //1.创建一个Thread类的子类
    public class MyThread extends Thread {
        //2.在Thread类的子类中重写Thread类中的run方法,设置线程任务(开启线程要做什么?)
        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
                System.out.println("run:" + i);
            }
        }
    }
    
    import package1.MyThread;
    
    /*
        创建多线程程序的第一种方式:创建Thread类的子类
        java.lang.Thread类:是描述线程的类,我们想要实现多线程程序,就必须继承Thread类
    
        实现步骤:
            1.创建一个Thread类的子类
            2.在Thread类的子类中重写Thread类中的run方法,设置线程任务(开启线程要做什么?)
            3.创建Thread类的子类对象
            4.调用Thread类中的方法start方法,开启新的线程,执行run方法
                 void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
                 结果是两个线程并发地运行;当前线程(main线程)和另一个线程(创建的新线程,执行其 run 方法)。
                 多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。
        java程序属于抢占式调度,那个线程的优先级高,那个线程优先执行;同一个优先级,随机选择一个执行
     */
    public class Solution {
        public static void main(String[] args) {
            //3.创建Thread类的子类对象
            MyThread mt = new MyThread();
            //4.调用Thread类中的方法start方法,开启新的线程,执行run方法
            mt.start();
    
            for (int i = 0; i < 20; i++) {
                System.out.println("main:" + i);
            }
        }
    }
    
  • 多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。

  • 多线程程序会开辟多个栈空间。

  • 获取线程的名称:

    • 使用Thread 类中的方法public String getName() 返回该线程的名称。
    • 先获取到当前正在执行的线程,使用线程中的方法getName() 获取线程的名称,public static Thread currentThread() 返回对当前正在执行的线程对象的引用。
    • main 线程所在类没有继承Thread,不能通过getName() 方法获取当前线程的名字,可以通过Thread.currentThread().getName() 获取。
  • main 线程的名字为 main,新线程的名字为 Thread-0Thread-1 等。

  • 设置线程名称:

    • 使用 Thread 类中的方法void setName(String name)
    • 创建一个带参数的构造方法,参数传递线程的名称;调用父类的带参构造方法,把线程名称传递给父类,让父类(Thread)给子线程起一个名字Thread(String name)
  • Thread 类常用方法汇总:

    • public Thread() :分配一个新的线程对象。
    • public Thread(String name):分配一个指定名字的新的线程对象。
    • public Thread(Runnable target) :分配一个带有指定目标新的线程对象。
    • public Thread(Runnable target,String name):分配一个带有指定目标新的线程对象并指定名字。
    • public String getName() :获取当前线程名称。
    • public void start():导致此线程开始执行; Java 虚拟机调用此线程的 run 方法。
    • public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
    • public static Thread currentThread():返回对当前正在执行的线程对象的引用。
  • 创建多线程的第二种方式:实现 java.lang.Runnable 接口

    1. 定义Runnable 接口的实现类,并重写该接口的run() 方法,该run() 方法的方法体同样是该线程的线程执行体。
    2. 创建Runnable 实现类的实例,并以此实例作为Threadtarget 来创建Thread 对象,该Thread 对象才是真正的线程对象。
    3. 调用线程对象的start() 方法来启动线程。
  • 实现 Runnable 接口创建多线程程序的好处:

    • 适合多个相同的程序代码的线程去共享同一个资源。
    • 避免了单继承的局限性(类继承了Thread 类就不能继承其他的类)。
    • 增强了程序的扩展性,降低了程序的耦合性(解耦),实现Runnable 接口的方式,把设置线程任务和开启新线程进行了分离(解耦)。
    • 线程池只能放入实现RunnableCallable 类线程,不能直接放入继承Thread 的类。
  • 关于 Runnable 适合资源共享的详细解释:

    • 继承Thread 类,多个线程分别完成自己的任务。

      class MyThread extends Thread {
          private int ticket = 10;
          private String name;
      
          public MyThread(String name) {
              this.name = name;
          }
      
          public void run() {
              for (int i = 0; i < 500; i++) {
                  if (this.ticket > 0) {
                      System.out.println(this.name + "卖票---->" + (this.ticket--));
                  }
              }
          }
      }
      
      public class ThreadDemo {
          public static void main(String[] args) {
              MyThread mt1 = new MyThread("一号窗口");
              MyThread mt2 = new MyThread("二号窗口");
              MyThread mt3 = new MyThread("三号窗口");
              mt1.start();
              mt2.start();
              mt3.start();
          }
      }
      
    • 实现Runnable 接口,多个线程共同完成一个任务。

      class MyThread1 implements Runnable {
          private int ticket = 10;
          private String name;
      
          public void run() {
              for (int i = 0; i < 500; i++) {
                  if (this.ticket > 0) {
                      System.out.println(Thread.currentThread().getName() + "卖票---->" + (this.ticket--));
                  }
              }
          }
      }
      
      public class RunnableDemo {
          public static void main(String[] args) {
              //设计三个线程
              MyThread1 mt = new MyThread1();
              Thread t1 = new Thread(mt, "一号窗口");
              Thread t2 = new Thread(mt, "二号窗口");
              Thread t3 = new Thread(mt, "三号窗口");
              t1.start();
              t2.start();
              t3.start();
          }
      }
      
    • Thread 为三个线程分别去完成三个任务,不容易实现资源共享,Runnable 为三个线程去完成一个任务,比较容易实现资源共享。

    • Thread 类实际上也是实现了 Runnable 接口的类,所以以下写法实际上还是通过实现Runnable 接口来实现多线程,通过public Thread(Runnable target) 来构造Thread

      public class ThreadDemo {
          public static void main(String[] args) {
              MyThread mt1 = new MyThread("");
              Thread t1 = new Thread(mt1, "窗口1");
              Thread t2 = new Thread(mt1, "窗口2");
              t1.start();
              t2.start();
          }
      }
      
  • java 中,每次程序运行至少启动 2 个线程。一个是 main 线程,一个是垃圾收集线程。因为每当使用 java 命令执行一个类的时候,实际上都会启动一个 JVM,每一个 JVM 其实在就是在操作系统中启动了一个进程。

  • 匿名内部类实现线程创建:

    public class NoNameInnerClassThread {
        public static void main(String[] args) {
            new Thread(new Runnable(){
                @Override
                public void run() {
                    for (int i = 0; i < 10; i++) {
                        System.out.println(Thread.currentThread().getName()+" "+i);
                    }
                }
            }).start();
        }
    }
    
  • 线程优先级
    优先级高的线程占用 cpu 多,但不一定先完成

    class ThreadPriority extends Thread {
    	@Override
    	public void run() {
    		for (int i = 0; i < 100; i++) {
    			System.out.println(getName() + ":" + i);
    		}
    	}
    }
    
    public class Main {
    	public static void main(String[] args) {
    		ThreadPriority tp1 = new ThreadPriority();
    		ThreadPriority tp2 = new ThreadPriority();
    		ThreadPriority tp3 = new ThreadPriority();
    		// 设置线程的名称
    		tp1.setName("高铁");
    		tp2.setName("飞机");
    		tp3.setName("汽车");
    		// 返回线程优先级
    		System.out.println(tp1.getPriority());
    		System.out.println(tp2.getPriority());
    		System.out.println(tp3.getPriority());
    		// 设置线程优先级
    		tp1.setPriority(5);
    		tp2.setPriority(10);
    		tp3.setPriority(1);
    		// 启动所有的线程
    		tp1.start();
    		tp2.start();
    		tp3.start();
    	}
    }
    
  • 在 Java 中有两类线程:用户线程 (User Thread)、守护线程 (Daemon Thread)。

    所谓 守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。

    用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。

    将线程转换为守护线程可以通过调用 Thread 对象的 setDaemon(true) 方法来实现。在使用守护线程时需要注意一下几点:

    • setDaemon(true) 必须在 thread.start() 之前设置,否则会跑出一个 IllegalThreadStateException 异常。你不能把正在运行的常规线程设置为守护线程。
    • 在 Daemon 线程中产生的新线程也是 Daemon 的。
    • 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
    class ThreadDaemon extends Thread {
    	@Override
    	public void run() {
    		for (int i = 0; i < 100; i++) {
    			System.out.println(getName() + ":" + i);
    		}
    	}
    }
    
    public class Main {
    	public static void main(String[] args) {
    		ThreadDaemon td1 = new ThreadDaemon();
    		ThreadDaemon td2 = new ThreadDaemon();
    		td1.setName("关羽");
    		td2.setName("张飞");
    		// 设置主线程为刘备
    		Thread.currentThread().setName("刘备");
    		// 设置守护线程
    		td1.setDaemon(true);
    		td2.setDaemon(true);
    		// 启动守护线程
    		td1.start();
    		td2.start();
    		// 执行主线程的逻辑
    		for (int i = 0; i < 10; i++) {
    			System.out.println(Thread.currentThread().getName() + ":" + i);
    		}
    	}
    }
    
  • join, t.join() 方法只会使主线程 (或者说调用 t.join() 的线程) 进入等待池并等待 t 线程执行完毕后才会被唤醒。并不影响同一时刻处在运行状态的其他线程。
    可以控制某个线程执行完之后再执行别的线程。

    class ThreadJoin extends Thread {
    	@Override
    	public void run() {
    		for (int i = 0; i < 100; i++) {
    			System.out.println(getName() + ":" + i);
    		}
    	}
    }
    
    public class Main {
    	public static void main(String[] args) {
    		ThreadJoin tj1 = new ThreadJoin();
    		ThreadJoin tj2 = new ThreadJoin();
    		ThreadJoin tj3 = new ThreadJoin();
    		tj1.setName("曹操");
    		tj2.setName("刘备");
    		tj3.setName("孙权");
    		tj1.start();
    		try {
    			tj1.join();
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    		tj2.start();
    		tj3.start();
    	}
    }
    

17.【线程、同步】

  • 线程安全问题:多线程同时对同一变量进行读写操作,就会出现线程不安全问题。

    package package1;
    
    public class RunnableImpl implements Runnable {
        private int ticket = 100;
    
        @Override
        public void run() {
            while (true) {
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                    ticket--;
                }
    
            }
        }
    }
    
    package package1;
    
    public class Main {
        public static void main(String[] args) {
            RunnableImpl r = new RunnableImpl();
            Thread t1 = new Thread(r, "窗口一");
            Thread t2 = new Thread(r, "窗口二");
            Thread t3 = new Thread(r, "窗口三");
            t1.start();
            t2.start();
            t3.start();
            //窗口一正在卖第100张票
            //窗口三正在卖第100张票
        }
    }
    
  • 线程安全问题产生的原理:

  • 线程同步的三种方式:

    • 同步代码块
    • 同步方法
    • 锁机制
  • 同步锁:

    • 对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁。
    • 锁对象可以是任意类型。
    • 多个线程对象要使用同一把锁。
    • 在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着
      (BLOCKED)。
  • 同步代码块:synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

    synchronized(同步锁){
        需要同步操作的代码
    }
    
    package package1;
    
    public class RunnableImpl implements Runnable {
        private int ticket = 10;
        //同步锁
        Object lock = new Object();
    
        @Override
        public void run() {
            while (true) {
                //同步代码块
                synchronized(lock){
                    if (ticket > 0) {
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                        ticket--;
                    }
                }
            }
        }
    }
    
  • 同步技术的原理:

  • 同步方法:使用 synchronized 修饰的方法,就叫做同步方法,保证 A 线程执行该方法的时候,其他线程只能在方法外等着。

    public synchronized void method(){
        可能会产生线程安全问题的代码
    }
    
    package package1;
    
    public class RunnableImpl implements Runnable {
        private int ticket = 10;
    
        @Override
        public void run() {
            while (true) {
                fun();
            }
        }
        public synchronized void fun(){
            if (ticket > 0) {
                System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                ticket--;
            }
        }
    }
    

    同步方法也会把方法内部的代码锁住,只让一个线程执行。

  • 同步方法的对象:

    • 对于非static 方法,同步锁就是this
    • 对于static 方法,我们使用当前方法所在类的字节码对象(类名。class)。
  • Lock 锁:java.util.concurrent.locks.lock 机制提供了比 sychronized 代码块和 synchronized 方法更广泛的锁定操作,同步代码块/同步方法具有的功能 Lock 都有,除此之外,更强大,更体现面向对象。

  • Lock 锁也称为同步锁:

    • public void lock():加同步锁
    • public void unlock():释放同步锁
  • Lock 为接口,java.util.concurrent.locks.ReentrantLock 实现了 Lock 接口。

  • Lock 锁的使用方法:

    package package1;
    
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class RunnableImpl implements Runnable {
        private int ticket = 10;
        //1.在成员位置创建一个ReentrantLock对象
        Lock lock = new ReentrantLock();
    
        @Override
        public void run() {
            while (true) {
                //2.在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁
                lock.lock();
                if (ticket > 0) {
                    //提高安全问题出现的概率,让程序睡眠
                    try {
                        Thread.sleep(10);
                        System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                        ticket--;
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        //3.在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁
                        lock.unlock();//无论程序是否异常,都会把锁释放,这样写更好
                    }
                }
            }
        }
    }
    
  • synchronized 是在 JVM 层面上实现的,如果代码执行出现异常,JVM 会自动释放锁,但是 Lock 不行,要保证锁一定会被释放,就必须将 unLock 放到 finally{} 中(手动释放)。

  • 线程的 6 种状态:

    线程状态导致状态发送条件
    NEW(新建)线程刚被创建,但是并未启动。还没有调用 start 方法。
    Runnable(可运行)线程可以在 Java 虚拟机中运行的状态,可能正在运行自己的代码,也可能没有,这取决于操作系统处理器。
    Blocked(锁阻塞)当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入 Blocked 状态;当该线程持有锁时,该线程将变为 Runnable 状态。
    Waiting(无限等待)一个线程在等待另一个线程执行唤醒动作时,该线程进入 Waiting 状态。进入这个状态后是不能自动唤醒的,必须等待另一个进程调用 notify 或者 notifyAll 方法才能够唤醒。
    Timed Waiting(计时等待)同 waiting 状态,有几个方法有超时参数,调用它们将进入 Timed Waiting 状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有 Thread.sleep、Object.wait。
    Terminated(被终止)因为 run 方法正常退出而死亡,或者因为没有捕获的异常终止了 run 方法而死亡。

  • Timed Waiting 可以被 notify 唤醒。

  • Obejct 类中的方法:

    • void wait():在其他线程调用此对象的notify() 方法或notifyAll() 方法前,导致当前线程等待。
    • void notify():唤醒在此对象监视器上等待的单个线程,会继续执行wait 方法之后的代码。
  • 只有锁对象才能调用 waitnotify 方法。

  • waitnotify 方法的使用案例:

    package package1;
    
    /*
        等待唤醒案例:线程之间的通信
            创建一个顾客线程(消费者):告知老板要的包子的种类和数量,调用wait方法,放弃cpu的执行,进入到WAITING状态(无限等待)
            创建一个老板线程(生产者):花了5秒做包子,做好包子之后,调用notify方法,唤醒顾客吃包子
    
        注意:
            顾客和老板线程必须使用同步代码块包裹起来,保证等待和唤醒只能有一个在执行
            同步使用的锁对象必须保证唯一
            只有锁对象才能调用wait和notify方法,配合synchronized关键字使用
    
        Obejct类中的方法
        void wait()
              在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。
        void notify()
              唤醒在此对象监视器上等待的单个线程。
              会继续执行wait方法之后的代码
     */
    public class Demo01WaitAndNotify {
        public static void main(String[] args) {
            //创建锁对象,保证唯一
            Object obj = new Object();
            // 创建一个顾客线程(消费者)
            new Thread() {
                @Override
                public void run() {
                    //一直等着买包子
                    while (true) {
                        //保证等待和唤醒的线程只能有一个执行,需要使用同步技术
                        synchronized (obj) {
                            System.out.println("告知老板要的包子的种类和数量");
                            //调用wait方法,放弃cpu的执行,进入到WAITING状态(无限等待)
                            try {
                                obj.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            //唤醒之后执行的代码
                            System.out.println("包子已经做好了,开吃!");
                            System.out.println("---------------------------------------");
                        }
                    }
                }
            }.start();
    
            //创建一个老板线程(生产者)
            new Thread() {
                @Override
                public void run() {
                    //一直做包子
                    while (true) {
                        //花了5秒做包子
                        try {
                            Thread.sleep(5000);//花5秒钟做包子
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
    
                        //保证等待和唤醒的线程只能有一个执行,需要使用同步技术
                        synchronized (obj) {
                            System.out.println("老板5秒钟之后做好包子,告知顾客,可以吃包子了");
                            //做好包子之后,调用notify方法,唤醒顾客吃包子
                            obj.notify();
                        }
                    }
                }
            }.start();
        }
    }
    
  • wait()notify() 方法调用的前提是,二者所在的线程体中(即 run 方法体)都需要先通过 synchronized 获取对象的锁(同步代码块或同步方法),否则会报异常 IllegalMonitorStateException,使用 ReentrantLock 的时候,用 Condition 对象来代替锁的 wait()notify() 以及 notifyAll() 方法。

  • 当线程调用 wait() 方法后,会释放锁对象,线程由 RUNNING 状态转向 WAITING 状态,即该线程进入等待队列中

    • wait() 方法释放锁对象之前,notifyThread 线程会阻塞在synchronized 获取锁对象的位置,而当wait() 释放锁后,notifyThread 线程会由阻塞状态尝试竞争锁,并拿到锁,开始执行同步块,当执行到notify() 方法后,会将waitThread 线程从等待队列中移动到调度队列中,即若锁不空闲,notify 方法只会触发waitThread 线程由WAITING 非竞争状态转化为BLOCKED 状态,即没有锁的时候,notify() 方法只是会触发wait() 所在的线程由非竞争的等待队列进入锁竞争的调度队列,由WAITING 状态转向BLOCKED 状态(从wait setentry set),想要执行wait() 方法后续代码的前提是waitThread 需要先竞争到锁对象,而由于notify() 所在的同步块目前正持有锁,所以在notify 从方法刚被调用(触发waitThread 参与竞争)到所在的同步块释放锁,这个过程里,waitThread 只能一直堵塞,若唤醒后可以获取锁,就会从WAITING 状态变成RUNNING 状态。

18.【线程池、Lambda 表达式】

  • 生产者与消费者问题:
    包子铺线程生产包子,吃货线程消费包子。
    当包子没有时(包子状态为 false),吃货线程等待,包子铺线程生产包子(即包子状态为 true),并通知吃货线程(解除吃货的等待状态),因为已经有包子了,那么包子铺线程进入等待状态。
    接下来,吃货线程能否进一步执行则取决于锁的获取情况。如果吃货获取到锁,那么就执行吃包子动作,包子吃完(包子状态为 false),并通知包子铺线程(解除包子铺的等待状态),吃货线程进入等待。包子铺线程能否进一步执行则取决于锁的获取情况。

    /*
        资源类:包子类
    	设置包子的属性
    		皮
    		陷
    		包子的状态: 有 true,没有 false
     */
    public class BaoZi {
        //皮
        String pi;
        //陷
        String xian;
        //包子的状态: 有 true,没有 false,设置初始值为false没有包子
        boolean flag = false;
    }
    
    /*
        生产者(包子铺)类:是一个线程类,可以继承Thread
    	设置线程任务(run):生产包子
    	对包子的状态进行判断
    	true:有包子
    		包子铺调用wait方法进入等待状态
    	false:没有包子
    		包子铺生产包子
    		增加一些趣味性:交替生产两种包子
    			有两种状态(i%2==0)
    		包子铺生产好了包子
    		修改包子的状态为true有
    		唤醒吃货线程,让吃货线程吃包子
    
    	注意:
    	    包子铺线程和包子线程关系-->通信(互斥)
    	    必须同时同步技术保证两个线程只能有一个在执行
    	    锁对象必须保证唯一,可以使用包子对象作为锁对象
    	    包子铺类和吃货的类就需要把包子对象作为参数传递进来
    	        1.需要在成员位置创建一个包子变量
    	        2.使用带参数构造方法,为这个包子变量赋值
     */
    public class BaoZiPu extends Thread{
        //1.需要在成员位置创建一个包子变量
        private BaoZi bz;
    
        //2.使用带参数构造方法,为这个包子变量赋值
        public BaoZiPu(BaoZi bz) {
            this.bz = bz;
        }
    
        //设置线程任务(run):生产包子
        @Override
        public void run() {
            //定义一个变量
            int count = 0;
            //让包子铺一直生产包子
            while(true){
                //必须同时同步技术保证两个线程只能有一个在执行
                synchronized (bz){
                    //对包子的状态进行判断
                    if(bz.flag==true){
                        //包子铺调用wait方法进入等待状态
                        try {
                            bz.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
    
                    //被唤醒之后执行,包子铺生产包子
                    //增加一些趣味性:交替生产两种包子
                    if(count%2==0){
                        //生产 薄皮三鲜馅包子
                        bz.pi = "薄皮";
                        bz.xian = "三鲜馅";
                    }else{
                        //生产 冰皮 牛肉大葱陷
                        bz.pi = "冰皮";
                        bz.xian = "牛肉大葱陷";
    
                    }
                    count++;
                    System.out.println("包子铺正在生产:"+bz.pi+bz.xian+"包子");
                    //生产包子需要3秒钟
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //包子铺生产好了包子
                    //修改包子的状态为true有
                    bz.flag = true;
                    //唤醒吃货线程,让吃货线程吃包子
                    bz.notify();
                    System.out.println("包子铺已经生产好了:"+bz.pi+bz.xian+"包子,吃货可以开始吃了");
                }
            }
        }
    }
    
    /*
        消费者(吃货)类:是一个线程类,可以继承Thread
    	设置线程任务(run):吃包子
    	对包子的状态进行判断
    	false:没有包子
    		吃货调用wait方法进入等待状态
    	true:有包子
    		吃货吃包子
    		吃货吃完包子
    		修改包子的状态为false没有
    		吃货唤醒包子铺线程,生产包子
     */
    public class ChiHuo extends Thread{
        //1.需要在成员位置创建一个包子变量
        private BaoZi bz;
    
        //2.使用带参数构造方法,为这个包子变量赋值
        public ChiHuo(BaoZi bz) {
            this.bz = bz;
        }
        //设置线程任务(run):吃包子
        @Override
        public void run() {
            //使用死循环,让吃货一直吃包子
            while (true){
                //必须同时同步技术保证两个线程只能有一个在执行
                synchronized (bz){
                    //对包子的状态进行判断
                    if(bz.flag==false){
                        //吃货调用wait方法进入等待状态
                        try {
                            bz.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
    
                    //被唤醒之后执行的代码,吃包子
                    System.out.println("吃货正在吃:"+bz.pi+bz.xian+"的包子");
                    //吃货吃完包子
                    //修改包子的状态为false没有
                    bz.flag = false;
                    //吃货唤醒包子铺线程,生产包子
                    bz.notify();
                    System.out.println("吃货已经把:"+bz.pi+bz.xian+"的包子吃完了,包子铺开始生产包子");
                    System.out.println("----------------------------------------------------");
                }
            }
        }
    }
    
    /*
        测试类:
    	包含main方法,程序执行的入口,启动程序
    	创建包子对象;
    	创建包子铺线程,开启,生产包子;
    	创建吃货线程,开启,吃包子;
     */
    public class Demo {
        public static void main(String[] args) {
            //创建包子对象;
            BaoZi bz =new BaoZi();
            //创建包子铺线程,开启,生产包子;
            new BaoZiPu(bz).start();
            //创建吃货线程,开启,吃包子;
            new ChiHuo(bz).start();
        }
    }
    
  • wait() 方法应该始终用在 while 循环而不是 if 中:当有一个消费者和一个生产者的时候 if 不会出错,当有多个生产者或者多个消费者的时候,if 就会出错。
    if 出错的关键在于:一个线程执行了 wait 方法以后,它不会再继续执行了,直到被 notify 唤醒,那么唤醒以后从何处开始执行?
    下面是一个生产者,两个消费者的代码:

    package package1;
    
    public class SynStack {
        private int cnt = 0;
        private int full = 1;
    
        public synchronized void push() {
            //此处应为while,不应该使用if
            if (cnt >= full) {
                try {
                    System.out.println("生产线程" + Thread.currentThread().getName() + "准备休眠了");
                    this.wait();
                    System.out.println("生产线程" + Thread.currentThread().getName() + "休眠结束了");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            cnt++;
            this.notify();
            System.out.println("生产线程" + Thread.currentThread().getName() + "正在生产第" + cnt + "个产品");
        }
    
        public synchronized void pop() {
            //此处应为while,不应该使用if
            if (cnt <= 0) {
                try {
                    System.out.println("消费线程" + Thread.currentThread().getName() + "准备休眠了");
                    this.wait();
                    System.out.println("消费线程" + Thread.currentThread().getName() + "休眠结束了");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    
            this.notify();
            System.out.println("消费线程" + Thread.currentThread().getName() + "正在消费第" + cnt + "个产品");
            cnt--;
        }
    }
    
    package package1;
    
    public class Producer implements Runnable {
        private SynStack ss = null;
    
        public Producer(SynStack ss) {
            this.ss = ss;
        }
    
        @Override
        public void run() {
            while (true) {
                ss.push();
            }
        }
    }
    
    package package1;
    
    public class Consumer implements Runnable {
        private SynStack ss = null;
    
        public Consumer(SynStack ss) {
            this.ss = ss;
        }
    
        @Override
        public void run() {
            while (true) {
                ss.pop();
            }
        }
    }
    
    package package1;
    
    public class Main {
        public static void main(String[] args) {
            SynStack ss = new SynStack();
            Producer producer = new Producer(ss);
            Consumer consumer1 = new Consumer(ss);
            Consumer consumer2 = new Consumer(ss);
    
            new Thread(producer, "1号").start();
            new Thread(consumer1, "6号").start();
            new Thread(consumer2, "7号").start();
        }
    }
    
    //运行结果
    生产线程1号正在生产第1个产品
    生产线程1号准备休眠了
    消费线程7号正在消费第1个产品
    消费线程7号准备休眠了
    消费线程6号准备休眠了
    生产线程1号休眠结束了
    生产线程1号正在生产第1个产品
    生产线程1号准备休眠了
    消费线程7号休眠结束了
    消费线程7号正在消费第1个产品
    消费线程7号准备休眠了
    消费线程6号休眠结束了
    消费线程6号正在消费第0个产品
    

    问题出现:6 号线程消费了第 0 个产品,为什么这样?
    7 号消费者唤醒了 6 号消费者,而 6 号消费者被唤醒以后会从 wait 后面开始执行,就会消费第 0 个产品,所以使用 wait 的话,应该使用 while 循环判断。

  • JDK1.5 后,内置了线程池。

  • 线程池的好处:

    • 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
    • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
    • 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约 1MB 内存,线程开的越多,消耗的内存也就越大,最后死机)。
  • 线程池的顶级接口为 java.util.concurrent.Executor,严格意义上讲 Executor 并不是一个线程池,只是一个执行线程的工具,真正的线程池接口是 java.util.concurrent.ExecutorService

  • 官方建议使用线程工厂类 java.concurrent.Executors 来创建线程池对象。

    • public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象(有界的线程池,参数为线程个数),使用ExecutorService 接口接受
  • 线程池提交线程:

    • public Future<?> submit(Runnable task):获取线程池中某一个线程对象,并执行
    • Future 接口:用来记录线程任务执行完毕后产生的结果
  • 关闭/销毁线程池的方法:void shutdown() 不建议使用。

  • 使用线程池的时候,不关闭线程池程序就不会停止。

  • 线程池使用样例:

    package com.itheima.demo02.ThreadPool;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    /*
        线程池:JDK1.5之后提供的
        java.util.concurrent.Executors:线程池的工厂类,用来生成线程池
        Executors类中的静态方法:
            static ExecutorService newFixedThreadPool(int nThreads) 创建一个可重用固定线程数的线程池
            参数:
                int nThreads:创建线程池中包含的线程数量
            返回值:
                ExecutorService接口,返回的是ExecutorService接口的实现类对象,我们可以使用ExecutorService接口接收(面向接口编程)
        java.util.concurrent.ExecutorService:线程池接口
            用来从线程池中获取线程,调用start方法,执行线程任务
                submit(Runnable task) 提交一个 Runnable 任务用于执行
            关闭/销毁线程池的方法
                void shutdown()
        线程池的使用步骤:
            1.使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
            2.创建一个类,实现Runnable接口,重写run方法,设置线程任务
            3.调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
            4.调用ExecutorService中的方法shutdown销毁线程池(不建议执行)
     */
    public class Demo01ThreadPool {
        public static void main(String[] args) {
            //1.使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
            ExecutorService es = Executors.newFixedThreadPool(2);
            //3.调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
            es.submit(new RunnableImpl());//pool-1-thread-1创建了一个新的线程执行
            //线程池会一直开启,使用完了线程,会自动把线程归还给线程池,线程可以继续使用
            es.submit(new RunnableImpl());//pool-1-thread-1创建了一个新的线程执行
            es.submit(new RunnableImpl());//pool-1-thread-2创建了一个新的线程执行
    
            //4.调用ExecutorService中的方法shutdown销毁线程池(不建议执行)
            es.shutdown();
    
            es.submit(new RunnableImpl());//抛异常,线程池都没有了,就不能获取线程了
        }
    
    }
    
  • Lambda 表达式的标准格式:(参数列表) -> { 重写方法的代码 }

    public class Main {
        public static void main(String[] args) {
            new Thread(() -> {
                System.out.println("Hello world!");
            }).start();
        }
    }
    
  • Lambda 的省略规则:

    • 小括号内参数类型可以省略。
    • 小括号内有且仅有一个参数时,小括号可以省略。
    • 大括号有且仅有一个语句,则无论是否有返回值,则可以省略大括号、return 关键字及语句分号,这三个必须一起省略或者不省略。
    public class Main {
        public static void main(String[] args) {
            new Thread(() -> System.out.println("Hello world!")).start();
            ArrayList<Person> list = new ArrayList<>();
            Collections.sort(list, (o1,o2) -> o1.getAge() - o2.getAge());
        }
    }
    
  • 使用 Lambda 的前提:

    • 必须具有接口,且要求接口中有且仅有一个抽象方法(例如RunnableComparator)。
    • 使用Lambda 必须具有上下文推断。
      也就是方法的参数或局部变量类型必须为Lambda 对应的接口类型,才能使用Lambda 作为该接口的实例。

    有且仅有一个抽象方法的接口,称之为函数式接口