avatar

Java基础教程

datas
发布时间
times139 min read

java HelloWorld 的执行过程以及原理

  • 第一步:启动 Java 虚拟机(JVM)
  • 第二步:虚拟机启动后,虚拟机会启动 类加载器 classloader
    • 类加载器的作用:加载类。本质上就是类加载器负责去硬盘上找"类"对应的"字节码"文件
    • 例如 java helloworld 那么类加载文件就会去找 helloworld.class 文件。
  • 第三步:类加载器如果在硬盘上找不到对应的字节码文件,会报错,无法加载主类
    • 类加载器如果找到对应的字节码文件,类加载器会将字节码文件装载到 JVM 当中,JVM 启动”解释器“将字节码解释为”101010000...“这种二进制码,操作系统执行二进制文件和硬件交互。

Java 数据类型

  • 基本数据类型 8 种

    • 整数型 byte(字节型)、short(短整型)、int(整型)、long (长整型)
    • 浮点型 double(双精度)、float(单精度)
    • 布尔型 boolean
    • 字符型 char
  • 引用数据类型:Java 中除了基本数据类型之外都是引用数据类型。

    • String 字符串不属于基本数据类型
  • 计算机存储单位

    • 计算机只能识别二进制。(10011100...)

    • 1 字节=8bit(8 比特)--> 1byte=8bit

    • 1bit 就是一个 1 或 0

    • 1KB=1024byte、1MB=1024KB、1GB=1024MB、1TB=1024GB

    • byte b=2:在计算机中表示为 00000010

    • short s=2:在计算机中表示为 00000000 00000010

    • int i=2:在计算机中表示为 00000000 00000000 00000010

    • byte-->1 字节(-128127) short-->2 字节(-3276832767) int-->4 字节(-2147483648~2147483647) long-->8 字节

    • float-->4 字节 double-->8 字节

    • boolean-->1 字节

    • char-->2 字节(0~65535)可以表示 65536 个不同的数字,和 short 相同

    •     int a=10;
          System.out.println(a);//默认十进制  10
          int b=010;
          System.out.println(b);//八进制  8
          int c=0x10;
          System.out.println(c);//十六进制 16
          int d=0b10;
          System.out.println(d);//二进制 2
      
  • ASCII('a'是 97 'A'是 65 '0'是 48)

  • 计算机原码反码补码

    • 计算机存储的是二进制补码形式

    • int i = 1;//正整数的原码反码补码相同
      //对应的二进制原码:00000000 00000000 00000000 00000001
      //对应的二进制反码:00000000 00000000 00000000 00000001
      //对应的二进制补码:00000000 00000000 00000000 00000001
      
      byte b = 1;
      //对应的二进制原码(第一位是符号位:负数的符号位为1):10000001
      //对应的二进制反码(符号位不变,其他位取反):10000001 ==》 11111110
      //对应的二进制补码(反码+1):11111110 ==》 11111111
      
  • 数据类型混合运算的时候取最大类型的字节在继续做运算

逻辑判断符

    //&与&&的区别
    int x = 10;
    int y = 11;
    System.out.println(x > y & x > y++);//false
    System.out.println(y);//12

    int n = 10;
    int m = 11;
    System.out.println(n > m && n > m++);//false
    System.out.println(m);//11

Java 方法

  • 普通方法

    • public class test02 {
          public static void main(String[] args) {
              Print_Str("打印一个方法体里面的内容");//打印一个方法体里面的内容
          }
          //[ 修饰符列表 ] 返回值类型 方法名称 方法参数
          //[]修饰符参数为可选参数
          //返回值类型包括基本数据类型和引用数据类型
          public static void Print_Str(String st) {
              //void表示空类型,没有返回值类型
              System.out.println(st);
              //方法体
      
              if(big(4,5)){
                  System.out.print("是的大于");
              }else {
                  System.out.print("不是小于");//不是小于
              }
          }
          public static Boolean big(int x,int y){
              if(x>y){
                  return true;
              }else {
                  return false;
              }
          }
      }
      
  • 方法的执行顺序

    • 方法体中的代码遵循自上而下的顺序依次执行

    • public class test03 {
          public static void main(String[] args) {
              print("main执行");
              m1();
              print("main结束");
          }
      
          public static void m1() {
              print("m1执行");
              m2();
              print("m1结束");
          }
      
          public static void m2() {
              print("m2执行");
              myClass.m3();
              print("m2结束");
          }
      
          public static void print(String str) {
              System.out.println(str);
          }
      }
      
      class myClass {
          public static void m3() {
              test03.print("m2执行");
              System.out.println("这是m3方法!");
              test03.print("m2结束");
          }
      }
      
      //打印输入结果
      
      main执行
      m1执行
      m2执行
      m2执行
      这是m3方法!
      m2结束
      m2结束
      m1结束
      main结束
      
    • break 和 return 的区别

      • break 控制的是循环;终止循环
      • return 控制的是方法;终止当前方法

Java 方法重载

  • 条件 1:在同一个类当中

  • 条件 2:方法名相同

  • 条件 3:

    • 参数列表不同

      • 参数个数不同
      • 参数类型不同
      • 参数顺序不同
    • 注意:

      • 和返回类型无关
      • 和修饰符无关
    • public class test06 {
          public static void main(String[] args) {
              System.out.println(m1(10, 1));//11
              double a = m1(10.8, 1.8);
              System.out.println(String.format("%.2f", a));//12.60
          }
      
          public static int m1(int x, int y) {
              return x + y;
          }
      
          public static double m1(double x, double y) {
              return x + y;
          }
      }
      
      //重载  println方法
      System.out.println(1.8d);
      System.out.println(1977777777777777777L);
      System.out.println(456);
      System.out.println(true);
      

Java 递归

  • public class RecursionTest02 {
        public static void main(String[] args) {
            int result=getSum(3);
            System.out.println(result);
        }
        public static int getSum(int n) {
            if (n == 1) {
                return 1;
            }
            return n + getSum(--n);
        }
    }
    

image-20210911220127437

面向对象和面向过程的区别

  • 从语言方面
    • 对于 c 语言来说,是完全面向过程的。
    • 对于 c++语言来说,是一半面向过程,一半是面向对象(c++半面向对象)
    • 对于 Java 和 c#语言来说,是完全面向对象的。
  • 面向过程开发
    • 面向过程的开发方式主要特点是:
      • 注重步骤,注重的是实现这个功能的步骤
      • 第一步做什么
      • 第二步做什么
      • ...
      • 另外面向过程也注重实现功能的因果关系
      • 因为 A 所以 B
      • 因为 B 所以 C
      • ...
      • 面向过程中没有对象的概念。只是实现这个功能的步骤以及因果关系。
    • 面向过程的优缺点
      • 缺点:耦合度高拓展力差
        • 面向过程最主要的是每一步与每一步的因果关系,其中 A 步骤因果关系到 B 步骤,A 和 B 联合起来形成一个子模块,子模块和子模块之间又因为因果关系结合在一起,假设其中任何一个因果关系出现问题(错误),此时整个系统的运转都会出现问题。(代码和代码之间的耦合度太高,拓展性太差)
          • 耦合度高导致拓展力差(集成显卡:计算机显卡不是独立显卡,是集成在主板上)
          • 耦合度低导致拓展力强(灯泡和灯口关系,螺栓和螺母关系)
          • 集成显卡和独立显卡=》面向过程和面向对象
      • 优点:快速开发
        • 对于小型项目(功能),采用面向过程的方式进行开发,效率较高。不需要前期进行对象的提取,模型的建立,采用面向过程方式可以直接开始干活。一上来直接写代码,编写因果关系。从而实现功能。
  • 面向对象开发
    • 面向对象的开发方式主要特点是:
      • 更符合人类的思维方式。(面向对象成为主流的原因)
      • 人类就是以对象的方式去认识世界的。
      • 面向对象就是将现实世界分割成不同的单元,然后每一个单元都实现成对象,然后给一个环境驱动一下。让各个对象之间协作起来形成一个系统。
      • image-20210912101442515
    • 特点:耦合度低,拓展力强。
      • image-20210912101639622
    • 面向过程主要关注的是:实现步骤以及整个过程。
    • 面向对象主要关注的是:对象 A,对象 B,对象 C,然后对象 ABC 组合,或者 CBA 组合...
  • 当我们采用面向对象的方式贯穿整个系统的话,涉及到的三个术语:
    • OOA:面向对象分析
    • OOD:面向对象设计
    • OOP:面向对象编程
    • 实现一个软件的过程:
      • 分析(A)-->设计(D)-->编程(p)
  • 类和对象
    • 类:是一个抽象的概念,不存在的,人类大脑思考总结一个模板(这个模板描述了共同特征),对象特征的总结
      • 具有共同特征,抽象出来的东西,就是一个类
    • 对象:实际存在的个体(人是一个类,姚明是一个对象...)
    • 实例:对象还有一个名字叫做实例 Stu s=new Stu(),s 为实例
    • 实例化:通过类这个模板创建对象的过程,叫做:实例化
    • 抽象:多个对象具有共同特征,进行思考总结抽取共同特征的过程
    • 类 -- 【实例化】--> 对象(实例)
    • 对象 -- 【抽象】--> 类
    • image-20210912104112097
    • image-20210912104204744
    • image-20210912112455941

Java 工程师与现实世界的关系

  • image-20210912114657300

创建对象

  • public class test07 {
        public static void main(String[] args) {
            Student s1=new Student();
            s1.name="张三";
            System.out.println(s1.name);
            System.out.println("-------------------");
            Student s2=new Student();
            s2.name="李四";
            System.out.println(s2.name);
        }
    }
    class Student {
        //学生名字
        String name;
        //学号
        int number;
        //性别
        boolean sex;
        //地址
        String address;
    }
    
  • image-20210912141118385

  • image-20210912165115840

  • 引用和对象区分

    • 引用是存储对象内存地址的一个变量。
    • 对象是堆内存里面 new 出来的。
  • package com.cobj.test01;
    
    public class Addres {
        //城市
        String city;
        //街道
        String street;
        //邮编
        int zipcode;
    }
    package com.cobj.test01;
    
    public class User {
        int id;//成员变量,实例变量(对象变量)
        String name;
        Addres addres;
    }
    package com.cobj.test01;
    //第一步:类加载
    public class Test {
        //第二步:test调用main方法(方法调用要压栈)
        //String name;   成员变量
        public static void main(String[] args){
            User u1=new User();
            u1.addres=new Addres();
            u1.addres.street="瑶湖区";
            u1.addres.city="南昌";
            u1.addres.zipcode=331200;
            u1.name="张三";
            System.out.println(u1.name+','+u1.addres.street+','+u1.addres.zipcode);
        }
    }
    
  • image-20210912172736452

对象回收

  • 垃圾回收器:GC

    • 在 Java 语言中,垃圾回收器主要针对的是**堆内存**
    • 当一个 Java 对象没有任何引用指向该对象的时候
    • GC 会考虑将该垃圾数据释放回收掉
  • image-20210912181838305

  • 空指针异常(NullPointerException)

    • **空引用**访问实例【对象相关】相关的数据时,都会出现空指针异常。

方法调用参数传递

  • Java 中关于方法调用时参数传递实际只有一个规则:

    • 在参数传递的时候,将变量中保存的那个复制一份过去
  • public class Test01 {
        public static void main(String[]args){
            int i=10;
            add(i);
            System.out.println("main=>"+i);//10
        }
        public static void add(int i){
            i++;
            System.out.println("add=>"+i);//11
        }
    }
    //基于对象传值
    public class Test {
        public static void main(String[] args) {
            Persion p = new Persion();
            p.age = 10;
            add(p);
            System.out.println("main=>" + p.age);//11
        }
    
        public static void add(Persion p) {
            p.age++;
            System.out.println("add=>" + p.age);//11
        }
    
    }
    
    class Persion {
        int age;
    }
    
  • image-20210912204619980

构造方法

  • 什么是构造方法,有什么用?

    • 构造方法是一个比较特殊的方法,通过构造方法可以完成对象的创建,以及实例变量的初始化。换句话说:构造方法是用来创建对象,并且同时给对象的属性赋值。(注意:实例变量没有手动赋值的时候,系统会赋默认值
    • 重点:当一个类没有提供任何构造方法,系统会默认提供一个无参数的构造方法。(而这个构造方法被称为缺省构造器)
  • 调用构造方法怎么调用

    • 使用 new 运算符来调用构造方法

    • public class Test {
          public static void main(String [] args){
              Student st=new Student(19,"张三","REP");
              System.out.println("年龄"+st.age);//年龄19
          }
      }
      
      public class Student {
          String name;
          int Number;
          int age;
          String hb;
          public Student(int age,String name,String hb) {//构造方法
              this.name=name;
              this.age=age;
              this.hb=hb;
              System.out.println("我是"+this.name);//张三
          }
      }
      
  • 构造方法的语法结构

    • 构造方法名和类名必须一致
  • 普通方法的语法结构

  • 实例变量没有手动赋值的时候,实际上系统会默认赋值

    • 赋值操作在什么时间进行
      • 实例变量是在构造方法执行的过程中完成初始化赋值
      • 错误 ❌ 不是在类加载的时候给实例变量赋值

封装

  • 面向对象的首要特征:封装 。什么是封装?有什么用?

    • 现实生活中有很多现实的例子都是封装的,

    • 例如:手机,电视机,笔记本电脑,照相机,这些都是外部有一个坚硬的壳儿。封装起来,保护内部的部件。保证内部的部件是安全的。另外封装了之后,对于我们使用者来说,我们是看不见内部的复杂结构的,我们也不需要关心内部有多么复杂,我们只需要操作外部壳儿上的几个按钮就可以完成操作。

    • 封装的作用有两个:

      • 第一个作用:保证内部结构的安全。
      • 第二个作用:屏蔽复杂,暴露简单。
    • 代码级别上的作用

      • 一个类体当中的数据,假设封装之后,对于代码的调用人员来说,不需要关心代码的复杂实现,只需要通过一个简单的入口就可以访问了。另外,类体中安全级别较高的数据封装起来,外部人员不能随意访问,来保证数据的安全性。
    • 怎么进行封装,代码怎么实现? 第一步:属性私有化(使用 private 关键字进行修饰。) 第二步:对外提供简单的操作入口。

  • public class Student {
        private String name;
        private int Number;
        private int age;
        private String hb;
    
        public Student(int age, String name, String hb) {
            this.name = name;
            this.age = age;
            this.hb = hb;
            System.out.println("我是" + this.name);//张三
        }
    
        public void setAge(int age) {
            if (age <= 0 || age >= 150) {
                System.out.println("年龄设置不合法");
                return;
            }
            this.age = age;
    
        }
    
        public int getAge() {
            return age;
        }
    }
    
    public class Test {
        public static void main(String [] args){
            Student st=new Student(19,"张三","REP");
            System.out.println("年龄"+st.getAge());//年龄19
            st.setAge(-18);
            System.out.println("年龄"+st.getAge());//年龄19
        }
    }
    

静态

  • class VarTest{
        /*
        *   成员变量:
        *       实例变量
        *       静态变量
        * */
    
        //以下实例的,都是对象相关的,访问时采用“引用.”的方式访问。需要先new对象
        //实例相关的,必须先有对象,才能访问,可能会出现空指针异常的情况。
        int i;//成员变量中的实例变量
        public void m2(){}//实例方法
    
        //以下静态的,都是类相关的,访问时采用“类名.”的方式访问。不需要new对象
        //不需要对象的参与即可访问。没有空指针异常的发生。
        //静态变量在类加载的时候初始化,不需要new对象,静态变量的空间就开出来了。
        //静态变量存储在方法区
        static int n;//成员变量中的静态变量
        public static void m1(){}//静态方法
    }
    
  • 静态变量

    • 如果这个类型的所有对象的某个属性都是一样的,不建议定义为实例变量,浪费内存空间。建议定义为类级别特征。定义为静态变量
  • image-20210913123448674

代码执行顺序

  • public class Test {
        //代码执行顺序
        static {
            System.out.println("A");
        }
    
        public static void main(String[] args) {
            System.out.println("C");
            new Test();
            System.out.println("F");
            //ABCDEF
        }
    
        public Test() {
            System.out.println("E");
        }
    
        {
            System.out.println("D");
        }
    
        static {
            System.out.println("B");
        }
    }
    

This

  • this 是一个关键字

  • 一个对象一个 this

    • this 是一个变量,是一个引用。this 保存当前对象的内存地址,指向自身。所以严格意义上来说,this 代表的就是当前对象,this 存储在堆内存当中对象的全部。
    • this 不能使用在静态方法中。
  • package com.This;
    
    public class Shop {
        String name;
        public Shop(String name){
            this.name=name;
        }
        public void Look(){
            System.out.println(this);
            System.out.println(this.name+"正在购物!");
        }
    }
    
    package com.This;
    
    public class Test {
        public static void main(String[] args) {
            Shop s=new Shop("张三");
            System.out.println(s);
            s.Look();
            //com.This.Shop@1b6d3586
            //com.This.Shop@1b6d3586
            //张三正在购物!
            Shop s1=new Shop("李四");
            System.out.println(s1);
            s1.Look();
            //com.This.Shop@4554617c
            //com.This.Shop@4554617c
            //李四正在购物!
        }
    }
    
  • this()

    • public Date(){
          //this.year=1970;
          //this.month=1;
          //this.day=1;
          this(1970,1,1);//更优
      }
      
      public Date(int year, int month, int day) {
          this.year = year;
          this.month = month;
          this.day = day;
      }
      
  • image-20210913210947832

变量内存图

  • image-20210913223444841

总结(实例、静态、构造)

  • public class Sutdent {//类
    
        static {
            System.out.println("Student类开始加载");
        }
        //类加载机制中:在程序执行之前,凡是需要加载的类全部加载到JVM当中。
        //先完成加载才会执行main方法
    
        public static void main(String[] args) {
            //局部变量
            int i = 100;
            Stu s = new Stu("张😀", 2021001);
            s.study();
        }
    }
    
    class Stu {
        static {
            System.out.println("Stu类开始加载");
        }
    
        /*
         类体{
            实例变量
            实例方法
    
            静态变量
            静态方法
    
            构造方法
    
            静态代码语句块
            实例代码语句块
    
            方法(){
                局部变量
                int i =100
            }
         }
         */
        private String name;//实例变量
        private int idCard;//实例变量
        static String job = "学生";//静态变量
    
        //构造方法
        public Stu() {
            this("未知学生名字", 00001);//this直接调用构造方法赋默认值
        }
    
        public Stu(String name, int idCard) {
            this.name = name;
            this.idCard = idCard;
        }
    
        //实例方法
        //静态方法的调用使用 “引用”.
        public void study() {
            //私有的是可以在本类访问,其他类中需要使用get取值set赋值
            //System.out.println(this.name+"正在学习!");
            //System.out.println(name+"正在学习!");
            //System.out.println(this.getName()+"正在学习!");
            System.out.println(getName() + "正在学习!");
            this.eat();
        }
    
        public void eat() {
            System.out.println(this.getName() + "正在吃饭!");
            Stu.m1();
        }
    
        //静态方法
        //静态方法的调用使用 “类名”.
        public static void m1() {
            System.out.println("执行了m1静态方法!");
            Stu.m2();
        }
    
        public static void m2() {
            System.out.println("m2方法访问静态变量Job:" + job);
        }
    
        //getter和setter方法
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getIdCard() {
            return idCard;
        }
    
        public void setIdCard(int idCard) {
            this.idCard = idCard;
        }
    }
    

继承

  • 继承的相关特性

    1. B 类继承 A 类,则称 A 类为超类(superclass)、父类、基类,B 类则称为子类(subclass)、派生类、扩展类。 class A class B extends A 我们平时聊天说的比较多的是:父类和子类。 superclass 父类 subclass 子类

    2. java 中的继承只支持单继承,不支持多继承,C++中支持多继承,这也是 java 体现简单性的一点,换句话说,java 中不允许这样写代码: class B extends A,C{ } 这是错误的。

    3. 虽然 java 中不支持多继承,但有的时候会产生间接继承的效果,例如:class C extends B,class B extends A,也就是说,C 直接继承 B,其实 C 还间接继承 A。

    4. java 中规定,子类继承父类,除构造方法不能继承之外,剩下都可以继承。但是私有的属性无法在子类中直接访问。父类中 private 修饰的不能在子类中直接访问。可以通过间接的手段来访问(getter 和 setter 访问、赋值)

    5. java 中的类没有显示的继承任何类,则默认继承 Object 类,Object 类是 java 语言提供的根类(老祖宗类),也就是说,一个对象与生俱来就有 Object 类型中所有的特征。

    6. 继承也存在一些缺点,例如:CreditAccount 类继承 Account 类会导致它们之间的耦合度非常高,Account 类发生改变之后会马上影响到 CreditAccount 类

    • public class InheriTest {
          public static void main(String [] args){
              Dog d=new Dog("小狗");
              d.eat();
              Cat c=new Cat("小猫");
              c.eat();
          }
      }
      class Animal{
          String name;
          public void eat(){
              System.out.println(this.name+"正在吃饭!");
          }
      }
      class Dog extends Animal{
          public Dog(String name){
              this.name=name;
          }
      }
      class Cat extends Animal{
          public Cat(String name){
              this.name=name;
          }
      }
      

System.out.println

  • public class Test {
    
        static Student stu = new Student();
    
        public static void main(String[] args) {
            stu.name = "张三";
            Test.stu.study();
            System.out.println("Hello word!");
        }
    }
    
    class Student {
        String name;
    
        public void study() {
            System.out.println(this.name + "正在学习");
        }
    }
    

方法覆盖

  • 条件一:两个类必须要有继承关系。

  • 条件二:重写之后的方法和之前的方法具有:

    • 相同的返回值类型
    • 相同的方法名
    • 相同的形式参数列表
  • 条件三:访问权限不能更低,可以更高(protected==>public)

  • 条件四:重写之后的方法不能比之前的方法抛出更多的异常,可以更少。

  • 注意:

    • 方法覆盖只是针对于方法,和属性无关
    • 私有方法无法覆盖
    • 构造方法不能被继承,所以构造方法不能被覆盖
    • 方法覆盖针对的是实例方法静态方法覆盖没有意义
  • public class Test {
        public static void main(String[]args){
            Dog d=new Dog("小花狗");
            d.move();
    
            Cat c=new Cat("小懒猫");
            c.move();
        }
    }
    class Animal{
        String name;
        public void move(){
            System.out.println(this.name+"正在移动!");
        }
    }
    class Dog extends Animal{
        public Dog(String name){
            this.name=name;
        }
    
        @Override
        public void move() {
            System.out.println(this.name+"正在奔跑!");
        }
    }
    class Cat extends Animal{
        public Cat(String name){
            this.name=name;
        }
    
        //@Override
        //public void move() {
        //    System.out.println(this.name+"正在路上爬!");
        //}
    }
    

多态

  • 父类型引用指向子类型对象

  • 包括编译阶段和运行阶段

    • 编译阶段:绑定父类的方法
      • 编译阶段编译器只知道 d 的类型是 Animal,所以编译器在检查语法的时候,会去 Animal.class 字节码中找 move()方法。进行静态绑定(前提 Animal 中有 move 方法)
    • 运行阶段:动态绑定子类型对象的方法
      • 运行阶段的时候,实际上在堆内存中创建的 Java 对象是 Dog 对象,所以 move 的时候,真正参与 move 的对象是 Dog 对象。动态执行 Dog 对象的 move()方法(动态绑定)
    • 多种形态 ,多种状态,编译和运行有两个不同的状态
  • public class Animal {
        String name;
    
        public void move() {
            System.out.println(this.name + "在移动!");
        }
    }
    public class Cat extends Animal {
        public Cat(String name) {
            this.name = name;
        }
    
        @Override
        public void move() {
            System.out.println(this.name + "在床上爬!");
        }
    }
    public class Dog extends Animal {
        public Dog(String name) {
            this.name = name;
        }
    
        @Override
        public void move() {
            System.out.println(this.name + "在路上跑");
        }
    
        public void likeFood(){
            System.out.println(this.name+"爱吃骨头");
        }
    }
    public class Test {
        public static void main(String[] args) {
            //向上转型
            Animal d=new Dog("小花狗");
            d.move();//小花狗在路上跑
    
            //向下转型
            Animal d1=new Dog("小狗狗");
            if(d1 instanceof Dog){//判断对象类型
                Dog dx=(Dog) d1;
                dx.likeFood();
            }
        }
    }
    

多态应用

  • public class Test {
        public static void main(String [] args){
            Person p=new Person();
            Dog d=new Dog();
            p.eats(d);
    
            Cat c=new Cat();
            p.eats(c);
        }
    }
    class Person{
        public void eats(Animal a){
            a.eat();
        }
        //面向父类型编程,面向更加抽象进行编程,不建议面向具体编程。面向具体编程会让软件的拓展力很差
        //public void eats(Dog a){
        //    a.eat();
        //}
        //public void eats(Cat a){
        //    a.eat();
        //}
    }
    class Animal{
        public void eat(){
    
        }
    }
    class Dog extends Animal{
    
        @Override
        public void eat() {
            System.out.println("狗正在吃骨头");
        }
    }
    class Cat extends Animal{
        @Override
        public void eat() {
            System.out.println("猫正在吃鱼");
        }
    }
    
  • 静态方法不谈覆盖 static

  • 私有方法不能覆盖 private

  • 多态在开发中的作用是: 降低程序的耦合度,提高程序的扩展力。

    public class Master{
    	public void feed(Dog d){}
    	public void feed(Cat c){}
    }
    

    以上的代码中表示:Master和Dog以及Cat的关系很紧密(耦合度高)。导致扩展力很差。

    public class Master{
    	public void feed(Pet pet){
    		pet.eat();
    	}
    }
    

    以上的代表中表示:Master和Dog以及Cat的关系就脱离了,Master关注的是Pet类。 这样Master和Dog以及Cat的耦合度就降低了,提高了软件的扩展性。

Super

  • super 能出现在实例方法和构造方法中。

    • super 的语法是:“super.”、“super()”
    • super 不能使用在静态方法中。
    • super. 大部分情况下是可以省略的。super.什么时候不能省略呢?
      • 父类和子类中有同名属性,或者说有同样的方法,
      • 想在子类中访问父类的,super. 不能省略。
    • super() 只能出现在构造方法第一行,通过当前的构造方法去调用“父类”中 的构造方法,目的是:创建子类对象的时候,先初始化父类型特征。
    • super 的使用: super.属性名 【访问父类的属性】 super.方法名(实参) 【访问父类的方法】 super(实参) 【调用父类的构造方法】
  • // 示例代码
    public class test02 {
        public static void main(String []args){
            Stu s=new Stu("张三",12,202101,"学生卡");
            System.out.println(s.getName()+",拥有:"+s.getType()+",卡号:"+s.getCid());
    
            Tea t=new Tea("梁老师",12,202101,"老师卡");
            System.out.println(t.getName()+",拥有:"+t.getType()+",卡号:"+t.getCid());
        }
    }
    class Card{
        private int cid;
        private String type;
    
        public Card(){
            System.out.println("Card");
        }
        public Card(String type,int cid){
            this.type=type;
            this.cid=cid;
        }
    
        public int getCid() {
            return cid;
        }
    
        public void setCid(int cid) {
            this.cid = cid;
        }
    
        public String getType() {
            return type;
        }
    
        public void setType(String type) {
            this.type = type;
        }
    }
    class Stu extends Card{
        private String name;
        private int age;
        public Stu(){
    
        }
        public Stu(String name,int age,int cid,String type){
            super(type,cid);
            this.name=name;
            this.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;
        }
    }
    class Tea extends Card{
        private String name;
        private int age;
        public Tea(){
    
        }
        public Tea(String name,int age,int cid,String type){
            //super(type,cid);
            this.name=name;
            this.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;
        }
    }
    

final

  • final 是 Java 语言中的一个关键字

  • final 表示最终的,不可变的,只能赋一次值

  • final 可以修饰变量、方法、类等

  • final 修饰的方法无法覆盖和重写

  • final 修饰的类无法继承

  • image-20211003205329800

  • final 修饰的实例变量,系统不负责赋默认值,必须手动赋值。在构造方法中赋值或者在变量后面赋值。

  • public class Tc {
        public static void main(String[] args) {
            BAB b = new BAB("张三");
            BAB V = new BAB("三");
        }
    }
    
    class BAB {
        final private String name;
        final private int age = 18;
    
        public BAB(String name) {
            this.name = name;//必须在构造方法中
        }
    
        public void setName(String name) {
    //        this.name=name;在此赋值报错
        }
    
        public String getName() {
            return name;
        }
    }
    

抽象类(abstract)

  • 抽象类定义

    • abstract class AbsTest{ }
      
  • 抽象类:无法实例化、无法创建对象,抽象类是用来被子类继承的

  • final 和 abstract 不能联合使用,这两个关键字是对立的。final 修饰的类无法继承

  • 抽象类的子类可以是抽象类,也可以是非抽象类

  • 抽象类虽然无法实例化,但是抽象类有构造方法,这个构造方法供子类使用的

  • 抽象类中不一定有抽象方法,抽象方法必须在抽象类中

  • 抽象方法定义

    • public abstract void dosome();
      
  • 一个非抽象类继承抽象类,必须将抽象类中的抽象方法进行覆盖、重写、实现

  • abstract class AbsTest {
        public abstract void dosome();
    }
    
    class Test extends AbsTest {
        @Override
        public void dosome() {
        }
    }
    

接口(interface )

  • 接口时一种“引用数据类型”

  • 接口时完全抽象的

  • 接口的定义

    • [修饰符列表] interface 接口名
  • 接口支持多继承

  • 接口中只有常量+抽象方法

  • 接口中所有的元素都是 public 修饰的

  • 接口中抽象方法的 public abstract 可以省略

  • 接口中常量的 public static final 可以省略

  • 接口中方法不能有方法体

  • 一个非抽象的类,实现接口的时候,必须将接口中所有方法加以实现

  • 一个类可以实现多个接口

  • extends 和 implements 可以共存,extends 在前,implements 在后

  • 使用接口,写代码的时候,可以使用多态(父类型引用指向子类型对象)

  • public class Test01 {
        public static void main(String[] args) {
            A e = new E();
            double pi = e.PI;
            System.out.println(pi);//3.1415926
        }
    }
    
    interface A {
        double PI = 3.1415926;
    
        void doSome();
    }
    
    interface B {
        void doEat();
    }
    
    interface C {
    }
    
    interface D extends A, B, C {
    }
    
    interface F {
        void doFly();
    }
    
    class E extends G implements D,F {
    
        @Override
        public void doSome() {
    
        }
    
        @Override
        public void doEat() {
    
        }
    
        @Override
        public void doFly() {
    
        }
    }
    class G{
    
    }
    

接口在开发中的作业

  • 注意:接口在开发中的作用,类似于多态在开发中的作用。

  • 多态:面向抽象编程,不要面向具体编程。降低程序的耦合度。提高程序的扩展力。

  • 三个字“解耦合”

    • 面向接口编程,可以降低程序的耦合度,提高程序的扩展力。符合 OCP 开发原则。
    • 接口的使用离不开多态机制。(接口+多态才可以达到降低耦合度。)
    • 接口可以解耦合,解开的是谁和谁的耦合!!! 任何一个接口都有调用者和实现者。 接口可以将调用者和实现者解耦合。 调用者面向接口调用。 实现者面向接口编写实现。
  • public class Test02 {
        public static void main(String[] args) {
            FoodMenu c = new WesternChef();   //实例化厨师
            User u = new User();              //实例化用户
            u.setFoodMenu(c);               //给用户菜单
            u.order();                      //用户已经点好餐
        }
    }
    
    //菜单接口
    interface FoodMenu {
        void YuxiangPork();
    
        void ScrambledEggs();
    }
    
    //厨师实现炒菜接口
    class ChinaChef implements FoodMenu {
    
        @Override
        public void YuxiangPork() {
            System.out.println("中国厨师做的鱼香肉丝");
        }
    
        @Override
        public void ScrambledEggs() {
            System.out.println("中国厨师做的西红柿草鸡蛋");
        }
    }
    
    class WesternChef implements FoodMenu {
        @Override
        public void YuxiangPork() {
            System.out.println("西餐厨师做的鱼香肉丝");
        }
    
        @Override
        public void ScrambledEggs() {
            System.out.println("西餐厨师做的西红柿炒鸡蛋");
        }
    }
    
    //用户点菜
    class User {
        private FoodMenu foodMenu;
    
        public User() {
        }
    
        public User(FoodMenu foodMenu) {
            this.foodMenu = foodMenu;
        }
    
        public FoodMenu getFoodMenu() {
            return foodMenu;
        }
    
        public void setFoodMenu(FoodMenu foodMenu) {
            this.foodMenu = foodMenu;
        }
    
        //用户点餐
        public void order() {
            //用户点了两个菜
            this.foodMenu.YuxiangPork();
            this.foodMenu.ScrambledEggs();
        }
    }
    

类型和类型之间的关系

  • is a(继承)、has a(关联)、like a(实现)
  • is a:
    • Cat is a Animal(猫是一个动物)
    • 凡是能够满足 is a 的表示“继承关系”
    • A extends B
  • has a:
    • I has a Pen(我有一支笔)
    • 凡是能够满足 has a 关系的表示“关联关系”
    • 关联关系通常以“属性”的形式存在。
    • java A{ B b; }
  • like a:
    • Cooker like a FoodMenu(厨师像一个菜单一样)
    • 凡是能够满足 like a 关系的表示“实现关系”
    • 实现关系通常是:类实现接口。
    • A implements B

抽象类和接口有什么区别

  • 在这里我们只说一下抽象类和接口在语法上的区别。
    至于以后抽象类和接口应该怎么进行选择,通过后面的项目去体会/学习。
    
    抽象类是半抽象的。
    接口是完全抽象的。
    
    抽象类中有构造方法。
    接口中没有构造方法。
    
    接口和接口之间支持多继承。
    类和类之间只能单继承。
    
    一个类可以同时实现多个接口。
    一个抽象类只能继承一个类(单继承)。
    
    接口中只允许出现常量和抽象方法。
    
    这里先透露一个信息:
    	以后接口使用的比抽象类多。一般抽象类使用的还是少。
    	接口一般都是对“行为”的抽象。
    

package 和 import

  • package

    • 第一:package 出现在 java 源文件第一行。
    • 第二:带有包名怎么编译?javac -d . xxx.java
    • 第三:怎么运行?java 完整类名
    • 补充:以后说类名的时候,如果带着包名描述,表示完整类名。如果没有带包,描述的话,表示简类名。
      • java.util.Scanner 完整类名。
      • Scanner 简类名
  • import

    • import 什么时候不需要?

      • java.lang 不需要。
      • 同包下不需要。
      • 其它一律都需要。
    • 怎么用?

      • import 完整类名;
      • import 包名.*;
      • import java.util.Scanner; // 完整类名。
    • 	// 同学的疑问:这样是不是效率比较低。
      	// 这个效率不低,因为编译器在编译的时候,会自动把*变成具体的类名。
      	import java.util.*;
      
      	// 想省懒劲你不能太省了。
      	import java.*; 这是不允许的,因为在java语言中规定,这里的*只代表某些类的名字。
      

访问控制权限

  • private 表示私有的,只能在本类中访问

  • public 表示公开的,在任何位置都可以访问

  • “默认”表示只能在本类,以及同包下访问。

  • protected 表示只能在本类、同包、子类中访问。

  • 访问控制修饰符本类同包子类任意位置
    public可以可以可以可以
    protected可以可以可以访问不到
    默认可以可以访问不到访问不到
    private可以访问不到访问不到访问不到

    范围从大到小排序:public > protected > 默认 > private

  • 访问控制符用在方法中

    • 属性(4 个都能用)
    • 方法(4 个都能用)
    • 类(public 和默认能用,其它不行。)
    • 接口(public 和默认能用,其它不行。)

数组

  • 数组是引用数据类型,所以数组对象是存储堆内存当中。

  • 数组一旦创建长度不可变

  • 数组中元素要求类型统一

  • 数组中的元素存储地址是连续的,内存地址连续,数组存储元素的特点

  • 数组的优缺点

    • 第一:空间存储上,内存地址是连续的。
    • 第二:每个元素占用的空间大小相同。
    • 第三:知道首元素的内存地址。
    • 第四:通过下标可以计算出偏移量。
    • 通过一个数学表达式,就可以快速计算出某个下标位置上元素的内存地址,直接通过内存地址定位,效率非常高
    • 优点:检索效率高
    • 缺点:随机增删效率较低,数组无法存储大数据量
    • 注意:数组最后一个元素的增删效率不受影响
  • 一维数组的静态初始化和动态初始化

    • 静态初始化:

      int[] arr = {1,2,3,4};
      Object[] objs = {new Object(), new Object(), new Object()};
      
    • 动态初始化:

      int[] arr = new int[4]; // 4个长度,每个元素默认值0
      Object[] objs = new Object[4]; // 4个长度,每个元素默认值null
      

冒泡排序

  • 冒泡排序算法
        1、每一次循环结束之后,都要找出最大的数据,放到参与比较的这堆数据的最右边。(冒出最大的那个气泡。)
        2、核心:
            拿着左边的数字和右边的数字比对,当左边 > 右边的时候,交换位置。
    原始数据:
    3, 2, 7, 6, 8
    1次循环:(最大的跑到最右边。)
    2, 3, 7, 6, 832比较,2 < 3,所以23交换位置)
    2, 3, 7, 6, 8 (虽然不需要交换位置:但是37还是需要比较一次。)
    2, 3, 6, 7, 876交换位置)
    2, 3, 6, 7, 8 (虽然不需要交换位置:但是37还是需要比较一次。)
    
    经过第1次循环,此时剩下参与比较的数据:2, 3, 6, 7
    2次循环:
    2, 3, 6, 7 (23比较,不需要交换位置)
    2, 3, 6, 736比较,不需要交换位置)
    2, 3, 6, 7 (67比较,不需要交换位置)
    
    经过第2次循环,此时剩下参与比较的数据:2, 3, 6
    3次循环:
    2, 3, 6 (23比较,不需要交换位置)
    2, 3, 636比较,不需要交换位置)
    
    经过第3次循环,此时剩下参与比较的数据:2, 3
    4次循环:
    2, 3 (23比较,不需要交换位置)
    
  • for (int i = 0; i < arr.length - 1; i++) {
        int m = i;
        for (int j = i + 1; j < arr.length; j++) {
            if (arr[m] > arr[j]) {
                m = j;
            }
        }
        if (m != i) {
            int temp = arr[m];
            arr[m] = arr[i];
            arr[i] = temp;
        }
    }
    

选择排序

  • 挑出小的,往前排序,减少交换次数

  • 选择排序比冒泡排序好在:每一次的交换位置都是有意义的

  • for (int i = 0; i < arr.length; i++) {
                for (int j = i + 1; j < arr.length; j++) {
                    if (arr[i] > arr[j]) {
                        int temp = arr[j];
                        arr[j] = arr[i];
                        arr[i] = temp;
                    }
                }
            }
    

二分查找

  • 二分法查找算法是基于排序的基础之上。(没有排序的数据是无法查找的。)

  • 二分法查找的终止条件:一直折半,直到中间的那个元素恰好是被查找的元素

  • public static void main(String[] args) {
        int[] arr = {3, 5, 7, 9, 12, 56, 78};
        int index = FindIndex(arr, 56);
        System.out.println("找到:" + index);
    }
    
    private static int FindIndex(int[] arr, int i) {
        if (arr.length == 0) return -1;
        int begin = 0;
        int end = arr.length - 1;
        while (begin <= end) {
            int midst = (begin + end) / 2;//求出中位数
            if (arr[midst] == i) {//判断是否等于要查询的数并返回
                return midst;
            } else if (arr[midst] > i) {
                //在左侧,已知开始的索引值
                end = midst - 1;
                for (int j = begin; j < end; j++) {
                    if (arr[j] == i) return j;
                }
            } else if (arr[midst] < i) {
                //在左侧,已知结束的索引值
                begin = midst + 1;
                for (int j = begin; j < end; j++) {
                    if (arr[j] == i) return j;
                }
            }
        }
        return -1;
    }
    

String

  • 对 String 在内存存储方面的理解:

    • 字符串一旦创建不可变。

    • 双引号括起来的字符串存储在字符串常量池中。

    • 字符串的比较必须使用 equals 方法。

    • String 已经重写了 toString()和 equals()方法。

    •     String st=new String("hello");
          String st1=new String("hello");
          //以上共创建了三对象:两个堆方法区的String对象,方法区字符串常量池一个
          System.out.println(st==st1);//false
          System.out.println(st.equals(st1));//true
          String st2="hello1";
          String st3="hello1";
          System.out.println(st2==st3);//true
      

    image-20211013223819275

    • 常用 API

      // String类当中常用方法。
      //1(掌握).char charAt(int index)
      char c = "中国人".charAt(1); // "中国人"是一个字符串String对象。只要是对象就能“点.”
      System.out.println(c); // 国
      
      // 2(了解).int compareTo(String anotherString)
      // 字符串之间比较大小不能直接使用 > < ,需要使用compareTo方法。
      int result = "abc".compareTo("abc");
      System.out.println(result); //0(等于0) 前后一致  10 - 10 = 0
      
      int result2 = "abcd".compareTo("abce");
      System.out.println(result2); //-1(小于0) 前小后大 8 - 9 = -1
      
      int result3 = "abce".compareTo("abcd");
      System.out.println(result3); // 1(大于0) 前大后小 9 - 8 = 1
      
      // 拿着字符串第一个字母和后面字符串的第一个字母比较。能分胜负就不再比较了。
      System.out.println("xyz".compareTo("yxz")); // -1
      
      // 3(掌握).boolean contains(CharSequence s)
      // 判断前面的字符串中是否包含后面的子字符串。
      System.out.println("HelloWorld.java".contains(".java")); // true
      System.out.println("http://www.baidu.com".contains("https://")); // false
      
      // 4(掌握). boolean endsWith(String suffix)
      // 判断当前字符串是否以某个子字符串结尾。
      System.out.println("test.txt".endsWith(".java")); // false
      System.out.println("test.txt".endsWith(".txt")); // true
      System.out.println("fdsajklfhdkjlsahfjkdsahjklfdss".endsWith("ss")); // true
      
      // 5(掌握).boolean equals(Object anObject)
      // 比较两个字符串必须使用equals方法,不能使用“==”
      // equals方法有没有调用compareTo方法? 老版本可以看一下。JDK13中并没有调用compareTo()方法。
      // equals只能看出相等不相等。
      // compareTo方法可以看出是否相等,并且同时还可以看出谁大谁小。
      System.out.println("abc".equals("abc")); // true
      
      // 6(掌握).boolean equalsIgnoreCase(String anotherString)
      // 判断两个字符串是否相等,并且同时忽略大小写。
      System.out.println("ABc".equalsIgnoreCase("abC")); // true
      
      // 7(掌握).byte[] getBytes()
      // 将字符串对象转换成字节数组
      byte[] bytes = "abcdef".getBytes();
      for(int i = 0; i < bytes.length; i++){
          System.out.println(bytes[i]);
      }
      
      // 8(掌握).int indexOf(String str)
      // 判断某个子字符串在当前字符串中第一次出现处的索引(下标)。
      System.out.println("oraclejavac++.netc#phppythonjavaoraclec++".indexOf("java")); // 6
      
      // 9(掌握).boolean isEmpty()
      // 判断某个字符串是否为“空字符串”。底层源代码调用的应该是字符串的length()方法。
      //String s = "";
      String s = "a";
      System.out.println(s.isEmpty());
      
      // 10(掌握). int length()
      // 面试题:判断数组长度和判断字符串长度不一样
      // 判断数组长度是length属性,判断字符串长度是length()方法。
      System.out.println("abc".length()); // 3
      
      System.out.println("".length()); // 0
      
      // 11(掌握).int lastIndexOf(String str)
      // 判断某个子字符串在当前字符串中最后一次出现的索引(下标)
      System.out.println("oraclejavac++javac#phpjavapython".lastIndexOf("java")); //22
      
      // 12(掌握). String replace(CharSequence target, CharSequence replacement)
      // 替换。
      // String的父接口就是:CharSequence
      String newString = "http://www.baidu.com".replace("http://", "https://");
      System.out.println(newString); //https://www.baidu.com
      // 把以下字符串中的“=”替换成“:”
      String newString2 = "name=zhangsan&password=123&age=20".replace("=", ":");
      System.out.println(newString2); //name:zhangsan&password:123&age:20
      
      // 13(掌握).String[] split(String regex)
      // 拆分字符串
      String[] ymd = "1980-10-11".split("-"); //"1980-10-11"以"-"分隔符进行拆分。
      for(int i = 0; i < ymd.length; i++){
          System.out.println(ymd[i]);
      }
      String param = "name=zhangsan&password=123&age=20";
      String[] params = param.split("&");
      for(int i = 0; i <params.length; i++){
          System.out.println(params[i]);
          // 可以继续向下拆分,可以通过“=”拆分。
      }
      
      // 14(掌握)、boolean startsWith(String prefix)
      // 判断某个字符串是否以某个子字符串开始。
      System.out.println("http://www.baidu.com".startsWith("http")); // true
      System.out.println("http://www.baidu.com".startsWith("https")); // false
      
      // 15(掌握)、 String substring(int beginIndex) 参数是起始下标。
      // 截取字符串
      System.out.println("http://www.baidu.com".substring(7)); //www.baidu.com
      
      // 16(掌握)、String substring(int beginIndex, int endIndex)
      // beginIndex起始位置(包括)
      // endIndex结束位置(不包括)
      System.out.println("http://www.baidu.com".substring(7, 10)); //www
      
      // 17(掌握)、char[] toCharArray()
      // 将字符串转换成char数组
      char[] chars = "我是中国人".toCharArray();
      for(int i = 0; i < chars.length; i++){
          System.out.println(chars[i]);
      }
      
      // 18(掌握)、String toLowerCase()
      // 转换为小写。
      System.out.println("ABCDefKXyz".toLowerCase());
      
      // 19(掌握)、String toUpperCase();
      System.out.println("ABCDefKXyz".toUpperCase());
      
      // 20(掌握). String trim();
      // 去除字符串前后空白
      System.out.println("           hello      world             ".trim());
      
      // 21(掌握). String中只有一个方法是静态的,不需要new对象
      // 这个方法叫做valueOf
      // 作用:将“非字符串”转换成“字符串”
      //String s1 = String.valueOf(true);
      //String s1 = String.valueOf(100);
      //String s1 = String.valueOf(3.14);
      
      // 这个静态的valueOf()方法,参数是一个对象的时候,会自动调用该对象的toString()方法吗?
      String s1 = String.valueOf(new Customer());
      //System.out.println(s1); // 没有重写toString()方法之前是对象内存地址 com.bjpowernode.javase.string.Customer@10f87f48
      System.out.println(s1); //我是一个VIP客户!!!!
      /*
      java.lang.StringBuilder
      StringBuffer和StringBuilder的区别?
          StringBuffer中的方法都有:synchronized关键字修饰。表示StringBuffer在多线程环境下运行是安全的。
          StringBuilder中的方法都没有:synchronized关键字修饰,表示StringBuilder在多线程环境下运行是不安全的。
      
          StringBuffer是线程安全的。
          StringBuilder是非线程安全的。
      
       */
      public class StringBuilderTest01 {
          public static void main(String[] args) {
      
              // 使用StringBuilder也是可以完成字符串的拼接。
              StringBuilder sb = new StringBuilder();
              sb.append(100);
              sb.append(true);
              sb.append("hello");
              sb.append("kitty");
              System.out.println(sb);
          }
      }
      

八种包装数据类型

  • Byte

  • Short

  • Integer

    • 		Integer n=100;
              Integer m=100;
              System.out.println(n==m);//true  ==>因为Jvm中整数常量池中的数据127~-128
              Integer n1=-100;
              Integer m1=-100;
              System.out.println(n1==m1);//true
              Integer n3=128;
              Integer m3=128;
              System.out.println(n3==m3);//false
              Integer n4=-129;
              Integer m4=-129;
              System.out.println(n4==m4);//false
      
  • Long

  • Float

  • Double

  • Boolean

  • Character

  • 数字类

    • DecimalFormat数字格式化
      		###,###.## 表示加入千分位,保留两个小数。
      		###,###.0000 表示加入千分位,保留4个小数,不够补0
      BigDecimal
      		财务软件中通常使用BigDecimal
      

异常处理 throws

  • 1、什么是异常,java提供异常处理机制有什么用?
        以下程序执行过程中发生了不正常的情况,而这种不正常的情况叫做:异常
        java语言是很完善的语言,提供了异常的处理方式,以下程序执行过程中出现了不正常情况,
        java把该异常信息打印输出到控制台,供程序员参考。程序员看到异常信息之后,可以对
        程序进行修改,让程序更加的健壮。
    
        什么是异常:程序执行过程中的不正常情况。
        异常的作用:增强程序的健壮性。
    
    2、以下程序执行控制台出现了:
        Exception in thread "main" java.lang.ArithmeticException: / by zero
           at com.bjpowernode.javase.exception.ExceptionTest01.main(ExceptionTest01.java:14)
       这个信息被我们称为:异常信息。这个信息是JVM打印的。
    
  • 处理异常的方式

    • 处理异常的第一种方式:
          在方法声明的位置上使用throws关键字抛出,谁调用我这个方法,我就抛给谁。抛给调用者来处理。
          这种处理异常的态度:上报。
      
      处理异常的第二种方式:
          使用try..catch语句对异常进行捕捉。
          这个异常不会上报,自己把这个事儿处理了。
          异常抛到此处为止,不再上抛了。
      
      注意:
          只要异常没有捕捉,采用上报的方式,此方法的后续代码不会执行。
          另外需要注意,try语句块中的某一行出现异常,该行后面的代码不会执行。
          try..catch捕捉异常之后,后续代码可以执行。
      
      在以后的开发中,处理编译时异常,应该上报还是捕捉呢,怎么选?
          如果希望调用者来处理,选择throws上报。
          其它情况使用捕捉的方式。
       */
      public class ExceptionTest06 {
          // 一般不建议在main方法上使用throws,因为这个异常如果真正的发生了,一定会抛给JVM。JVM只有终止。
          // 异常处理机制的作用就是增强程序的健壮性。怎么能做到,异常发生了也不影响程序的执行。所以
          // 一般main方法中的异常建议使用try..catch进行捕捉。main就不要继续上抛了。
          /*
          public static void main(String[] args) throws FileNotFoundException {
              System.out.println("main begin");
              m1();
              System.out.println("main over");
          }
           */
          public static void main(String[] args) {
      
              // 100 / 0这是算术异常,这个异常是运行时异常,你在编译阶段,可以处理,也可以不处理。编译器不管。
              //System.out.println(100 / 0); // 不处理编译器也不管
              // 你处理也可以。
              /*
              try {
                  System.out.println(100 / 0);
              } catch(ArithmeticException e){
                  System.out.println("算术异常了!!!!");
              }
               */
      
              System.out.println("main begin");
              try {
                  // try尝试
                  m1();
                  // 以上代码出现异常,直接进入catch语句块中执行。
                  System.out.println("hello world!");
              } catch (FileNotFoundException e){ // catch后面的好像一个方法的形参。
                  // 这个分支中可以使用e引用,e引用保存的内存地址是那个new出来异常对象的内存地址。
                  // catch是捕捉异常之后走的分支。
                  // 在catch分支中干什么?处理异常。
                  System.out.println("文件不存在,可能路径错误,也可能该文件被删除了!");
                  System.out.println(e); //java.io.FileNotFoundException: D:\course\01-课\学习方法.txt (系统找不到指定的路径。)
              }
      
              // try..catch把异常抓住之后,这里的代码会继续执行。
              System.out.println("main over");
          }
      
          private static void m1() throws FileNotFoundException {
              System.out.println("m1 begin");
              m2();
              // 以上代码出异常,这里是无法执行的。
              System.out.println("m1 over");
          }
      
          // 抛别的不行,抛ClassCastException说明你还是没有对FileNotFoundException进行处理
          //private static void m2() throws ClassCastException{
          // 抛FileNotFoundException的父对象IOException,这样是可以的。因为IOException包括FileNotFoundException
          //private static void m2() throws IOException {
          // 这样也可以,因为Exception包括所有的异常。
          //private static void m2() throws Exception{
          // throws后面也可以写多个异常,可以使用逗号隔开。
          //private static void m2() throws ClassCastException, FileNotFoundException{
          private static void m2() throws FileNotFoundException {
              System.out.println("m2 begin");
              // 编译器报错原因是:m3()方法声明位置上有:throws FileNotFoundException
              // 我们在这里调用m3()没有对异常进行预处理,所以编译报错。
              // m3();
      
              m3();
              // 以上如果出现异常,这里是无法执行的!
              System.out.println("m2 over");
          }
      
          private static void m3() throws FileNotFoundException {
              // 调用SUN jdk中某个类的构造方法。
              // 这个类还没有接触过,后期IO流的时候就知道了。
              // 我们只是借助这个类学习一下异常处理机制。
              // 创建一个输入流对象,该流指向一个文件。
              /*
              编译报错的原因是什么?
                  第一:这里调用了一个构造方法:FileInputStream(String name)
                  第二:这个构造方法的声明位置上有:throws FileNotFoundException
                  第三:通过类的继承结构看到:FileNotFoundException父类是IOException,IOException的父类是Exception,
                  最终得知,FileNotFoundException是编译时异常。
      
                  错误原因?编译时异常要求程序员编写程序阶段必须对它进行处理,不处理编译器就报错。
               */
              //new FileInputStream("D:\\course\\01-开课\\学习方法.txt");
      
              // 我们采用第一种处理方式:在方法声明的位置上使用throws继续上抛。
              // 一个方法体当中的代码出现异常之后,如果上报的话,此方法结束。
              new FileInputStream("D:\\course\\01-课\\学习方法.txt");
      
              System.out.println("如果以上代码出异常,这里会执行吗??????????????????不会!!!");
          }
      }
      
  • final finally finalize 有什么区别?

    •  final 关键字
              final修饰的类无法继承
              final修饰的方法无法覆盖
              final修饰的变量不能重新赋值。
      
          finally 关键字
      try一起联合使用。
              finally语句块中的代码是必须执行的。
      
          finalize 标识符
              是一个Object类中的方法名。
              这个方法是由垃圾回收器GC负责调用的。
      

集合

  • ArrayList:底层是数组
  • LinkedList:底层是双向链表
  • Vector:底层是数组,线程安全的,效率较低,使用较少
  • HashSet:底层是 HashMap,放到 HashSet 集合中的元素等同于放到 HashMap 集合中 Key 部分
  • TreeSet:底层是 TreeMap,放到 TreeSet 集合中的元素等同于放到 TreeMap 集合中 key 部分
  • HashMap:底层是哈希表
  • Hashtable:底层是哈希表,线程安全的,效率较低使用较少
  • Properties:底层是哈希表,线程安全的,key 和 value 只能存储字符串 String
  • TreeMap:底层是二叉树。TreeMap 集合中的 key 可以自动按照大小顺序排序
  • List 集合存储元素特点:
    • 有序可重复
    • 有序:存进去和取出来的顺序相同,每个元素都是有下标的
    • 可以重复:存进去
  • Set(Map)集合存储元素特点:
    • 无序不可以重复
    • 无序:存进去和取出来的顺序不一定相同,另外 Set 集合中元素没有下标
    • 不可重复:存进去 1,就不能再存进去 1 了
  • SortedSet(SortedMap)集合存储元素特点:
    • 首先是无序不可重复的,但是 SortedSet 集合中的元素是可排序的
    • 无序:存进去和取出来的下标不一定相同,另外 Set 集合没有元素下标
    • 不可重复:就不能存入相同的值
    • 可排序:可以按照大小顺序排序
  • Map 集合中的 Key,就是一个 Set 集合
  • 在 Set 集合中放数据,实际上是放在了 Map 集合中的 Key 部分

Collection

package Contains01;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.Objects;

public class Test01 {
    public static void main(String[] args) {
        Collection arr = new ArrayList();
        arr.add(123);
        arr.add(new String("123"));
        arr.add("123");
        System.out.println(arr.contains("123"));//true
        arr.remove("123");
        System.out.println(arr.size());//2

        User u1 = new User("ZH");
        User u2 = new User("ZH");
        arr.add(u1);
        System.out.println(arr.contains(u2));//在重写equals前为false,重写equals后为true
        //contains比较的是内容不是内存地址。remove删除比较的是equals
        System.out.println("============");
        //创建迭代器迭代对象
        Iterator iterator=arr.iterator();
        while (iterator.hasNext()){
            System.out.println(iterator.next());
        }
    }
}

class User {
    private String name;

    public User() {

    }

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(name, user.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name);
    }
}

ArrayList

ArrayList集合底层采用了数组这种数据结构
ArrayList集合是非线程安全的
1、ArrayList集合初始化容量是10
2、ArrayList集合底层是Object类型的数组Object[]
3、扩容到原容量的1.5倍
4、建议给定一个预估计的初始化容量,减少数组扩容次数,这是Arraylist集合比较重要的优化策略。
5、数组优点:检索效率高
6、数组缺点:随机增删效率比较低,无法存储大容量数据
7、向数组末尾添加元素,效率很高,不受影响
8、用的比较多,扩容的问题拖慢效率

danlinkdata

/*
单链表中的节点。
节点是单向链表中基本的单元。
每一个节点Node都有两个属性:
    一个属性:是存储的数据。
    另一个属性:是下一个节点的内存地址。
 */
public class Node {

    // 存储的数据
    Object data;

    // 下一个节点的内存地址
    Node next;

    public Node(){

    }

    public Node(Object data, Node next){
        this.data = data;
        this.next = next;
    }
}

/*
链表类。(单向链表)
 */
public class Link<E> {
    public static void main(String[] args) {
        Link<String> link = new Link<>();
        link.add("abc");

        // 类型不匹配。
//        link.add(1);
    }

    // 头节点
    Node header;

    int size = 0;

    public int size(){
        return size;
    }

    // 向链表中添加元素的方法(向末尾添加)
    public void add(E data){
    //public void add(Object data){
        // 创建一个新的节点对象
        // 让之前单链表的末尾节点next指向新节点对象。
        // 有可能这个元素是第一个,也可能是第二个,也可能是第三个。
        if(header == null){
            // 说明还没有节点。
            // new一个新的节点对象,作为头节点对象。
            // 这个时候的头节点既是一个头节点,又是一个末尾节点。
            header = new Node(data, null);
        }else {
            // 说明头不是空!
            // 头节点已经存在了!
            // 找出当前末尾节点,让当前末尾节点的next是新节点。
            Node currentLastNode = findLast(header);
            currentLastNode.next = new Node(data, null);
        }
        size++;
    }

    /**
     * 专门查找末尾节点的方法。
     */
    private Node findLast(Node node) {
        if(node.next == null) {
            // 如果一个节点的next是null
            // 说明这个节点就是末尾节点。
            return node;
        }
        // 程序能够到这里说明:node不是末尾节点。
        return findLast(node.next); // 递归算法!
    }

    // 删除链表中某个数据的方法
    public void remove(Object obj){

    }

    // 修改链表中某个数据的方法
    public void modify(Object newObj){

    }

    // 查找链表中某个元素的方法。
    public int find(Object obj){
        return 1;
    }
}

LinkedList

LinkedList集合底层采用了双向链表数据接口
1、LinkedList集合是双向链表
2、对于链表数据结构,随机增删的效率较高,检索效率低
3、链表中的元素,在空间存储上,内存地址不连续

shuangxianglianbiao

vector

/*
Vector:
    1、底层也是一个数组。
    2、初始化容量:10
    3、怎么扩容的?
        扩容之后是原容量的2倍。
        10--> 20 --> 40 --> 80

    4、ArrayList集合扩容特点:
        ArrayList集合扩容是原容量1.5倍。

    5、Vector中所有的方法都是线程同步的,都带有synchronized关键字,
    是线程安全的。效率比较低,使用较少了。

    6、怎么将一个线程不安全的ArrayList集合转换成线程安全的呢?
        使用集合工具类:
            java.util.Collections;

            java.util.Collection 是集合接口。
            java.util.Collections 是集合工具类。
 */
public class VectorTest {
    public static void main(String[] args) {
        // 创建一个Vector集合
        List vector = new Vector();
        //Vector vector = new Vector();

        // 添加元素
        // 默认容量10个。
        vector.add(1);
        vector.add(2);
        vector.add(3);
        vector.add(4);
        vector.add(5);
        vector.add(6);
        vector.add(7);
        vector.add(8);
        vector.add(9);
        vector.add(10);

        // 满了之后扩容(扩容之后的容量是20.)
        vector.add(11);

        Iterator it = vector.iterator();
        while(it.hasNext()){
            Object obj = it.next();
            System.out.println(obj);
        }

        // 这个可能以后要使用!!!!
        List myList = new ArrayList(); // 非线程安全的。

        // 变成线程安全的
        Collections.synchronizedList(myList); // 这里没有办法看效果,因为多线程没学,你记住先!

        // myList集合就是线程安全的了。
        myList.add("111");
        myList.add("222");
        myList.add("333");
    }
}

泛型机制

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/*
1、JDK5.0之后推出的新特性:泛型
2、泛型这种语法机制,只在程序编译阶段起作用,只是给编译器参考的。(运行阶段泛型没用!)
3、使用了泛型好处是什么?
    第一:集合中存储的元素类型统一了。
    第二:从集合中取出的元素类型是泛型指定的类型,不需要进行大量的“向下转型”!

4、泛型的缺点是什么?
    导致集合中存储的元素缺乏多样性!
    大多数业务中,集合中元素的类型还是统一的。所以这种泛型特性被大家所认可。
 */
public class GenericTest01 {
    public static void main(String[] args) {

        /*
        // 不使用泛型机制,分析程序存在缺点
        List myList = new ArrayList();

        // 准备对象
        Cat c = new Cat();
        Bird b = new Bird();

        // 将对象添加到集合当中
        myList.add(c);
        myList.add(b);

        // 遍历集合,取出每个Animal,让它move
        Iterator it = myList.iterator();
        while(it.hasNext()) {
            // 没有这个语法,通过迭代器取出的就是Object
            //Animal a = it.next();

            Object obj = it.next();
            //obj中没有move方法,无法调用,需要向下转型!
            if(obj instanceof Animal){
                Animal a = (Animal)obj;
                a.move();
            }
        }
         */

        // 使用JDK5之后的泛型机制
        // 使用泛型List<Animal>之后,表示List集合中只允许存储Animal类型的数据。
        // 用泛型来指定集合中存储的数据类型。
        List myList = new ArrayList<Animal>();

        // 指定List集合中只能存储Animal,那么存储String就编译报错了。
        // 这样用了泛型之后,集合中元素的数据类型更加统一了。
        //myList.add("abc");

        Cat c = new Cat();
        Bird b = new Bird();

        myList.add(c);
        myList.add(b);

        // 获取迭代器
        // 这个表示迭代器迭代的是Animal类型。
        Iterator<Animal> it = myList.iterator();
        while(it.hasNext()){
            // 使用泛型之后,每一次迭代返回的数据都是Animal类型。
            //Animal a = it.next();
            // 这里不需要进行强制类型转换了。直接调用。
            //a.move();

            // 调用子类型特有的方法还是需要向下转换的!
            Animal a = it.next();
            if(a instanceof Cat) {
                Cat x = (Cat)a;
                x.catchMouse();
            }
            if(a instanceof Bird) {
                Bird y = (Bird)a;
                y.fly();
            }
        }
    }
}

class Animal {
    // 父类自带方法
    public void move(){
        System.out.println("动物在移动!");
    }
}

class Cat extends Animal {
    // 特有方法
    public void catchMouse(){
        System.out.println("猫抓老鼠!");
    }
}

class Bird extends Animal {
    // 特有方法
    public void fly(){
        System.out.println("鸟儿在飞翔!");
    }
}

自动类型推断机制

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/*
JDK之后引入了:自动类型推断机制。(又称为钻石表达式)
 */
public class GenericTest02 {
    public static void main(String[] args) {

        // ArrayList<这里的类型会自动推断>(),前提是JDK8之后才允许。
        // 自动类型推断,钻石表达式!
        List<Animal> myList = new ArrayList<>();

        myList.add(new Animal());
        myList.add(new Cat());
        myList.add(new Bird());

        // 遍历
        Iterator<Animal> it = myList.iterator();
        while(it.hasNext()){
            Animal a = it.next();
            a.move();
        }

        List<String> strList = new ArrayList<>();

        // 类型不匹配。
        //strList.add(new Cat());
        strList.add("http://www.126.com");
        strList.add("http://www.baidu.com");
        strList.add("http://www.bjpowernode.com");

        // 类型不匹配。
        //strList.add(123);

        //System.out.println(strList.size());

        // 遍历
        Iterator<String> it2 = strList.iterator();
        while(it2.hasNext()){
            // 如果没有使用泛型
            /*
            Object obj = it2.next();
            if(obj instanceof String){
                String ss = (String)obj;
                ss.substring(7);
            }
             */
            // 直接通过迭代器获取了String类型的数据
            String s = it2.next();
            // 直接调用String类的substring方法截取字符串。
            String newString = s.substring(7);
            System.out.println(newString);
        }
    }
}

自定义泛型

/*
自定义泛型可以吗?可以
    自定义泛型的时候,<> 尖括号中的是一个标识符,随便写。
    java源代码中经常出现的是:
        <E>和<T>
    E是Element单词首字母。
    T是Type单词首字母。
 */
public class GenericTest03<标识符随便写> {

    public void doSome(标识符随便写 o){
        System.out.println(o);
    }

    public static void main(String[] args) {

        // new对象的时候指定了泛型是:String类型
        GenericTest03<String> gt = new GenericTest03<>();

        // 类型不匹配
        //gt.doSome(100);

        gt.doSome("abc");

        // =============================================================
        GenericTest03<Integer> gt2 = new GenericTest03<>();
        gt2.doSome(100);

        // 类型不匹配
        //gt2.doSome("abc");

        MyIterator<String> mi = new MyIterator<>();
        String s1 = mi.get();

        MyIterator<Animal> mi2 = new MyIterator<>();
        Animal a = mi2.get();

        // 不用泛型就是Object类型。
        /*GenericTest03 gt3 = new GenericTest03();
        gt3.doSome(new Object());*/
    }
}

class MyIterator<T> {
    public T get(){
        return null;
    }
}

Foreach

/*
JDK5.0之后推出了一个新特性:叫做增强for循环,或者叫做foreach
 */
public class ForEachTest01 {
    public static void main(String[] args) {

        // int类型数组
        int[] arr = {432,4,65,46,54,76,54};

        // 遍历数组(普通for循环)
        for(int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }

        // 增强for(foreach)
        // 以下是语法
        /*for(元素类型 变量名 : 数组或集合){
            System.out.println(变量名);
        }*/

        System.out.println("======================================");
        // foreach有一个缺点:没有下标。在需要使用下标的循环中,不建议使用增强for循环。
        for(int data : arr) {
            // data就是数组中的元素(数组中的每一个元素。)
            System.out.println(data);
        }
    }
}
/*
集合使用foreach
 */
public class ForEachTest02 {
    public static void main(String[] args) {
        // 创建List集合
        List<String> strList = new ArrayList<>();

        // 添加元素
        strList.add("hello");
        strList.add("world!");
        strList.add("kitty!");

        // 遍历,使用迭代器方式
        Iterator<String> it = strList.iterator();
        while(it.hasNext()){
            String s = it.next();
            System.out.println(s);
        }

        // 使用下标方式(只针对于有下标的集合)
        for(int i = 0; i < strList.size(); i++){
            System.out.println(strList.get(i));
        }

        // 使用foreach
        for(String s : strList){ // 因为泛型使用的是String类型,所以是:String s
            System.out.println(s);
        }

        List<Integer> list = new ArrayList<>();
        list.add(100);
        list.add(200);
        list.add(300);
        for(Integer i : list){ // i代表集合中的元素
            System.out.println(i);
        }
    }
}

HashSet

import java.util.Set;
import java.util.TreeSet;

/*
TreeSet集合存储元素特点:
    1、无序不可重复的,但是存储的元素可以自动按照大小顺序排序!
    称为:可排序集合。

    2、无序:这里的无序指的是存进去的顺序和取出来的顺序不同。并且没有下标。
 */
public class TreeSetTest01 {
    public static void main(String[] args) {
        // 创建集合对象
        Set<String> strs = new TreeSet<>();
        // 添加元素
        strs.add("A");
        strs.add("B");
        strs.add("Z");
        strs.add("Y");
        strs.add("Z");
        strs.add("K");
        strs.add("M");
        // 遍历
        /*
            A
            B
            K
            M
            Y
            Z
        从小到大自动排序!
         */
        for(String s : strs){
            System.out.println(s);
        }
    }
}

TreeSet

import java.util.Set;
import java.util.TreeSet;

/*
TreeSet集合存储元素特点:
    1、无序不可重复的,但是存储的元素可以自动按照大小顺序排序!
    称为:可排序集合。

    2、无序:这里的无序指的是存进去的顺序和取出来的顺序不同。并且没有下标。
 */
public class TreeSetTest01 {
    public static void main(String[] args) {
        // 创建集合对象
        Set<String> strs = new TreeSet<>();
        // 添加元素
        strs.add("A");
        strs.add("B");
        strs.add("Z");
        strs.add("Y");
        strs.add("Z");
        strs.add("K");
        strs.add("M");
        // 遍历
        /*
            A
            B
            K
            M
            Y
            Z
        从小到大自动排序!
         */
        for(String s : strs){
            System.out.println(s);
        }
    }
}

Map

常用接口

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/*
java.util.Map接口中常用的方法:
    1、Map和Collection没有继承关系。
    2、Map集合以key和value的方式存储数据:键值对
        key和value都是引用数据类型。
        key和value都是存储对象的内存地址。
        key起到主导的地位,value是key的一个附属品。
    3、Map接口中常用方法:
        V put(K key, V value) 向Map集合中添加键值对
        V get(Object key) 通过key获取value
        void clear()    清空Map集合
        boolean containsKey(Object key) 判断Map中是否包含某个key
        boolean containsValue(Object value) 判断Map中是否包含某个value
        boolean isEmpty()   判断Map集合中元素个数是否为0
        V remove(Object key) 通过key删除键值对
        int size() 获取Map集合中键值对的个数。
        Collection<V> values() 获取Map集合中所有的value,返回一个Collection

        Set<K> keySet() 获取Map集合所有的key(所有的键是一个set集合)

        Set<Map.Entry<K,V>> entrySet()
            将Map集合转换成Set集合
            假设现在有一个Map集合,如下所示:
                map1集合对象
                key             value
                ----------------------------
                1               zhangsan
                2               lisi
                3               wangwu
                4               zhaoliu

                Set set = map1.entrySet();
                set集合对象
                1=zhangsan 【注意:Map集合通过entrySet()方法转换成的这个Set集合,Set集合中元素的类型是 Map.Entry<K,V>】
                2=lisi     【Map.Entry和String一样,都是一种类型的名字,只不过:Map.Entry是静态内部类,是Map中的静态内部类】
                3=wangwu
                4=zhaoliu ---> 这个东西是个什么?Map.Entry
 */
public class MapTest01 {
    public static void main(String[] args) {
        // 创建Map集合对象
        Map<Integer, String> map = new HashMap<>();
        // 向Map集合中添加键值对
        map.put(1, "zhangsan"); // 1在这里进行了自动装箱。
        map.put(2, "lisi");
        map.put(3, "wangwu");
        map.put(4, "zhaoliu");
        // 通过key获取value
        String value = map.get(2);
        System.out.println(value);
        // 获取键值对的数量
        System.out.println("键值对的数量:" + map.size());
        // 通过key删除key-value
        map.remove(2);
        System.out.println("键值对的数量:" + map.size());
        // 判断是否包含某个key
        // contains方法底层调用的都是equals进行比对的,所以自定义的类型需要重写equals方法。
        System.out.println(map.containsKey(new Integer(4))); // true
        // 判断是否包含某个value
        System.out.println(map.containsValue(new String("wangwu"))); // true

        // 获取所有的value
        Collection<String> values = map.values();
        // foreach
        for(String s : values){
            System.out.println(s);
        }

        // 清空map集合
        map.clear();
        System.out.println("键值对的数量:" + map.size());
        // 判断是否为空
        System.out.println(map.isEmpty()); // true
    }
}

遍历

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

/*
Map集合的遍历。【非常重要】
 */
public class MapTest02 {
    public static void main(String[] args) {

        // 第一种方式:获取所有的key,通过遍历key,来遍历value
        Map<Integer, String> map = new HashMap<>();
        map.put(1, "zhangsan");
        map.put(2, "lisi");
        map.put(3, "wangwu");
        map.put(4, "zhaoliu");
        // 遍历Map集合
        // 获取所有的key,所有的key是一个Set集合
        Set<Integer> keys = map.keySet();
        // 遍历key,通过key获取value
        // 迭代器可以
        /*Iterator<Integer> it = keys.iterator();
        while(it.hasNext()){
            // 取出其中一个key
            Integer key = it.next();
            // 通过key获取value
            String value = map.get(key);
            System.out.println(key + "=" + value);
        }*/
        // foreach也可以
        for(Integer key : keys){
            System.out.println(key + "=" + map.get(key));
        }

        // 第二种方式:Set<Map.Entry<K,V>> entrySet()
        // 以上这个方法是把Map集合直接全部转换成Set集合。
        // Set集合中元素的类型是:Map.Entry
        Set<Map.Entry<Integer,String>> set = map.entrySet();
        // 遍历Set集合,每一次取出一个Node
        // 迭代器
        /*Iterator<Map.Entry<Integer,String>> it2 = set.iterator();
        while(it2.hasNext()){
            Map.Entry<Integer,String> node = it2.next();
            Integer key = node.getKey();
            String value = node.getValue();
            System.out.println(key + "=" + value);
        }*/

        // foreach
        // 这种方式效率比较高,因为获取key和value都是直接从node对象中获取的属性值。
        // 这种方式比较适合于大数据量。
        for(Map.Entry<Integer,String> node : set){
            System.out.println(node.getKey() + "--->" + node.getValue());
        }
    }
}

HashMap

import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/*
HashMap集合:
    1、HashMap集合底层是哈希表/散列表的数据结构。
    2、哈希表是一个怎样的数据结构呢?
        哈希表是一个数组和单向链表的结合体。
        数组:在查询方面效率很高,随机增删方面效率很低。
        单向链表:在随机增删方面效率较高,在查询方面效率很低。
        哈希表将以上的两种数据结构融合在一起,充分发挥它们各自的优点。
    3、HashMap集合底层的源代码:
        public class HashMap{
            // HashMap底层实际上就是一个数组。(一维数组)
            Node<K,V>[] table;
            // 静态的内部类HashMap.Node
            static class Node<K,V> {
                final int hash; // 哈希值(哈希值是key的hashCode()方法的执行结果。hash值通过哈希函数/算法,可以转换存储成数组的下标。)
                final K key; // 存储到Map集合中的那个key
                V value; // 存储到Map集合中的那个value
                Node<K,V> next; // 下一个节点的内存地址。
            }
        }
        哈希表/散列表:一维数组,这个数组中每一个元素是一个单向链表。(数组和链表的结合体。)
    4、最主要掌握的是:
        map.put(k,v)
        v = map.get(k)
        以上这两个方法的实现原理,是必须掌握的。
    5、HashMap集合的key部分特点:
        无序,不可重复。
        为什么无序? 因为不一定挂到哪个单向链表上。
        不可重复是怎么保证的? equals方法来保证HashMap集合的key不可重复。
        如果key重复了,value会覆盖。

        放在HashMap集合key部分的元素其实就是放到HashSet集合中了。
        所以HashSet集合中的元素也需要同时重写hashCode()+equals()方法。

    6、哈希表HashMap使用不当时无法发挥性能!
        假设将所有的hashCode()方法返回值固定为某个值,那么会导致底层哈希表变成了
        纯单向链表。这种情况我们成为:散列分布不均匀。
        什么是散列分布均匀?
            假设有100个元素,10个单向链表,那么每个单向链表上有10个节点,这是最好的,
            是散列分布均匀的。
        假设将所有的hashCode()方法返回值都设定为不一样的值,可以吗,有什么问题?
            不行,因为这样的话导致底层哈希表就成为一维数组了,没有链表的概念了。
            也是散列分布不均匀。
        散列分布均匀需要你重写hashCode()方法时有一定的技巧。
    7、重点:放在HashMap集合key部分的元素,以及放在HashSet集合中的元素,需要同时重写hashCode和equals方法。
    8、HashMap集合的默认初始化容量是16,默认加载因子是0.75
        这个默认加载因子是当HashMap集合底层数组的容量达到75%的时候,数组开始扩容。

        重点,记住:HashMap集合初始化容量必须是2的倍数,这也是官方推荐的,
        这是因为达到散列均匀,为了提高HashMap集合的存取效率,所必须的。
 */
public class HashMapTest01 {
    public static void main(String[] args) {
        // 测试HashMap集合key部分的元素特点
        // Integer是key,它的hashCode和equals都重写了。
        Map<Integer,String> map = new HashMap<>();
        map.put(1111, "zhangsan");
        map.put(6666, "lisi");
        map.put(7777, "wangwu");
        map.put(2222, "zhaoliu");
        map.put(2222, "king"); //key重复的时候value会自动覆盖。

        System.out.println(map.size()); // 4

        // 遍历Map集合
        Set<Map.Entry<Integer,String>> set = map.entrySet();
        for(Map.Entry<Integer,String> entry : set){
            // 验证结果:HashMap集合key部分元素:无序不可重复。
            System.out.println(entry.getKey() + "=" + entry.getValue());
        }
    }

哈希表存储数据的特点

hashmap

import java.util.HashSet;
import java.util.Set;

/*
1、向Map集合中存,以及从Map集合中取,都是先调用key的hashCode方法,然后再调用equals方法!
equals方法有可能调用,也有可能不调用。
    拿put(k,v)举例,什么时候equals不会调用?
        k.hashCode()方法返回哈希值,
        哈希值经过哈希算法转换成数组下标。
        数组下标位置上如果是null,equals不需要执行。
    拿get(k)举例,什么时候equals不会调用?
        k.hashCode()方法返回哈希值,
        哈希值经过哈希算法转换成数组下标。
        数组下标位置上如果是null,equals不需要执行。

2、注意:如果一个类的equals方法重写了,那么hashCode()方法必须重写。
并且equals方法返回如果是true,hashCode()方法返回的值必须一样。
    equals方法返回true表示两个对象相同,在同一个单向链表上比较。
    那么对于同一个单向链表上的节点来说,他们的哈希值都是相同的。
    所以hashCode()方法的返回值也应该相同。

3、hashCode()方法和equals()方法不用研究了,直接使用IDEA工具生成,但是这两个方法需要同时生成。

4、终极结论:
    放在HashMap集合key部分的,以及放在HashSet集合中的元素,需要同时重写hashCode方法和equals方法。

5、对于哈希表数据结构来说:
    如果o1和o2的hash值相同,一定是放到同一个单向链表上。
    当然如果o1和o2的hash值不同,但由于哈希算法执行结束之后转换的数组下标可能相同,此时会发生“哈希碰撞”。

 */
public class HashMapTest02 {
    public static void main(String[] args) {

        Student s1 = new Student("zhangsan");
        Student s2 = new Student("zhangsan");

        // 重写equals方法之前是false
        //System.out.println(s1.equals(s2)); // false

        // 重写equals方法之后是true
        System.out.println(s1.equals(s2)); //true (s1和s2表示相等)

        System.out.println("s1的hashCode=" + s1.hashCode()); //284720968 (重写hashCode之后-1432604525)
        System.out.println("s2的hashCode=" + s2.hashCode()); //122883338 (重写hashCode之后-1432604525)

        // s1.equals(s2)结果已经是true了,表示s1和s2是一样的,相同的,那么往HashSet集合中放的话,
        // 按说只能放进去1个。(HashSet集合特点:无序不可重复)
        Set<Student> students = new HashSet<>();
        students.add(s1);
        students.add(s2);
        System.out.println(students.size()); // 这个结果按说应该是1. 但是结果是2.显然不符合HashSet集合存储特点。怎么办?
    }
}
import java.util.HashMap;
import java.util.Map;

/*
HashMap集合key部分允许null吗?
    允许
    但是要注意:HashMap集合的key null值只能有一个。
    有可能面试的时候遇到这样的问题。
 */
public class HashMapTest03 {
    public static void main(String[] args) {

        Map map = new HashMap();

        // HashMap集合允许key为null
        map.put(null, null);
        System.out.println(map.size()); // 1

        // key重复的话value是覆盖!
        map.put(null, 100);
        System.out.println(map.size()); //1

        // 通过key获取value
        System.out.println(map.get(null)); // 100
    }
}

Hashtable

import java.util.Hashtable;
import java.util.Map;

/*
Hashtable的key可以为null吗?
    Hashtable的key和value都是不能为null的。
    HashMap集合的key和value都是可以为null的。

Hashtable方法都带有synchronized:线程安全的。
线程安全有其它的方案,这个Hashtable对线程的处理
导致效率较低,使用较少了。

Hashtable和HashMap一样,底层都是哈希表数据结构。
Hashtable的初始化容量是11,默认加载因子是:0.75f
Hashtable的扩容是:原容量 * 2 + 1
 */
public class HashtableTest01 {
    public static void main(String[] args) {
        Map map = new Hashtable();

        //map.put(null, "123");
        map.put(100, null);

    }
}

Properties

import java.util.Properties;

/*
目前只需要掌握Properties属性类对象的相关方法即可。
Properties是一个Map集合,继承Hashtable,Properties的key和value都是String类型。
Properties被称为属性类对象。
Properties是线程安全的。
 */
public class PropertiesTest01 {
    public static void main(String[] args) {

        // 创建一个Properties对象
        Properties pro = new Properties();

        // 需要掌握Properties的两个方法,一个存,一个取。
        pro.setProperty("url", "jdbc:mysql://localhost:3306/bjpowernode");
        pro.setProperty("driver","com.mysql.jdbc.Driver");
        pro.setProperty("username", "root");
        pro.setProperty("password", "123");

        // 通过key获取value
        String url = pro.getProperty("url");
        String driver = pro.getProperty("driver");
        String username = pro.getProperty("username");
        String password = pro.getProperty("password");

        System.out.println(url);
        System.out.println(driver);
        System.out.println(username);
        System.out.println(password);

    }
}

TreeSet

存储元素特点

/*
TreeSet集合存储元素特点:
    1、无序不可重复的,但是存储的元素可以自动按照大小顺序排序!
    称为:可排序集合。

    2、无序:这里的无序指的是存进去的顺序和取出来的顺序不同。并且没有下标。
 */
public class TreeSetTest01 {
    public static void main(String[] args) {
        // 创建集合对象
        Set<String> strs = new TreeSet<>();
        // 添加元素
        strs.add("A");
        strs.add("B");
        strs.add("Z");
        strs.add("Y");
        strs.add("Z");
        strs.add("K");
        strs.add("M");
        // 遍历
        /*
            A
            B
            K
            M
            Y
            Z
        从小到大自动排序!
         */
        for(String s : strs){
            System.out.println(s);
        }
    }
}

TreeSet 底层是 TreeMap

/*
1、TreeSet集合底层实际上是一个TreeMap
2、TreeMap集合底层是一个二叉树。
3、放到TreeSet集合中的元素,等同于放到TreeMap集合key部分了。
4、TreeSet集合中的元素:无序不可重复,但是可以按照元素的大小顺序自动排序。
称为:可排序集合。
 */
public class TreeSetTest02 {
    public static void main(String[] args) {
        // 创建一个TreeSet集合
        TreeSet<String> ts = new TreeSet<>();
        // 添加String
        ts.add("zhangsan");
        ts.add("lisi");
        ts.add("wangwu");
        ts.add("zhangsi");
        ts.add("wangliu");
        // 遍历
        for(String s : ts){
            // 按照字典顺序,升序!
            System.out.println(s);
        }

        TreeSet<Integer> ts2 = new TreeSet<>();
        ts2.add(100);
        ts2.add(200);
        ts2.add(900);
        ts2.add(800);
        ts2.add(600);
        ts2.add(10);
        for(Integer elt : ts2){
            // 升序!
            System.out.println(elt);
        }
    }
}

/*
数据库中有很多数据:
    userid  name     birth
    -------------------------------------
    1       zs          1980-11-11
    2       ls          1980-10-11
    3       ww          1981-11-11
    4       zl          1979-11-11

    编写程序从数据库当中取出数据,在页面展示用户信息的时候按照生日升序或者降序。
    这个时候可以使用TreeSet集合,因为TreeSet集合放进去,拿出来就是有顺序的。
 */

自定义排序

import java.util.TreeSet;

public class TreeSetTest04 {
    public static void main(String[] args) {
        Customer c1 = new Customer(32);
        Customer c2 = new Customer(20);
        Customer c3 = new Customer(30);
        Customer c4 = new Customer(25);

        // 创建TreeSet集合
        TreeSet<Customer> customers = new TreeSet<>();
        // 添加元素
        customers.add(c1);
        customers.add(c2);
        customers.add(c3);
        customers.add(c4);

        // 遍历
        for (Customer c : customers) {
            System.out.println(c);
        }
    }
}

// 放在TreeSet集合中的元素需要实现java.lang.Comparable接口。
// 并且实现compareTo方法。equals可以不写。
class Customer implements Comparable<Customer> {

    int age;

    public Customer(int age) {
        this.age = age;
    }

    // 需要在这个方法中编写比较的逻辑,或者说比较的规则,按照什么进行比较!
    // k.compareTo(t.key)
    // 拿着参数k和集合中的每一个k进行比较,返回值可能是>0 <0 =0
    // 比较规则最终还是由程序员指定的:例如按照年龄升序。或者按照年龄降序。
    @Override
    public int compareTo(Customer c) { // c1.compareTo(c2);
        // this是c1
        // c是c2
        // c1和c2比较的时候,就是this和c比较。
        /*int age1 = this.age;
        int age2 = c.age;
        if(age1 == age2){
            return 0;
        } else if(age1 > age2) {
            return 1;
        } else {
            return -1;
        }*/
        //return this.age - c.age; // =0 >0 <0
        return c.age - this.age;
    }

    public String toString() {
        return "Customer[age=" + age + "]";
    }
}
//编写测试
import java.util.TreeSet;

/*
先按照年龄升序,如果年龄一样的再按照姓名升序。
 */
public class TreeSetTest05 {
    public static void main(String[] args) {
        TreeSet<Vip> vips = new TreeSet<>();
        vips.add(new Vip("zhangsi", 20));
        vips.add(new Vip("zhangsan", 20));
        vips.add(new Vip("king", 18));
        vips.add(new Vip("soft", 17));
        for(Vip vip : vips){
            System.out.println(vip);
        }
    }
}

class Vip implements Comparable<Vip>{
    String name;
    int age;

    public Vip(String name, int age) {
        this.name = name;
        this.age = age;
    }

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

    /*
    compareTo方法的返回值很重要:
        返回0表示相同,value会覆盖。
        返回>0,会继续在右子树上找。【10 - 9 = 1 ,1 > 0的说明左边这个数字比较大。所以在右子树上找。】
        返回<0,会继续在左子树上找。
     */
    @Override
    public int compareTo(Vip v) {
        // 写排序规则,按照什么进行比较。
        if(this.age == v.age){
            // 年龄相同时按照名字排序。
            // 姓名是String类型,可以直接比。调用compareTo来完成比较。
            return this.name.compareTo(v.name);
        } else {
            // 年龄不一样
            return this.age - v.age;
        }
    }
}

Comparable 和 Comparator

import java.util.Comparator;
import java.util.TreeSet;

/*
TreeSet集合中元素可排序的第二种方式:使用比较器的方式。
最终的结论:
    放到TreeSet或者TreeMap集合key部分的元素要想做到排序,包括两种方式:
        第一种:放在集合中的元素实现java.lang.Comparable接口。
        第二种:在构造TreeSet或者TreeMap集合的时候给它传一个比较器对象。
Comparable和Comparator怎么选择呢?
    当比较规则不会发生改变的时候,或者说当比较规则只有1个的时候,建议实现Comparable接口。
    如果比较规则有多个,并且需要多个比较规则之间频繁切换,建议使用Comparator接口。

    Comparator接口的设计符合OCP原则。
 */
public class TreeSetTest06 {
    public static void main(String[] args) {
        // 创建TreeSet集合的时候,需要使用这个比较器。
        // TreeSet<WuGui> wuGuis = new TreeSet<>();//这样不行,没有通过构造方法传递一个比较器进去。

        // 给构造方法传递一个比较器。
        //TreeSet<WuGui> wuGuis = new TreeSet<>(new WuGuiComparator());

        // 大家可以使用匿名内部类的方式(这个类没有名字。直接new接口。)
        TreeSet<WuGui> wuGuis = new TreeSet<>(new Comparator<WuGui>() {
            @Override
            public int compare(WuGui o1, WuGui o2) {
                return o1.age - o2.age;
            }
        });

        wuGuis.add(new WuGui(1000));
        wuGuis.add(new WuGui(800));
        wuGuis.add(new WuGui(810));

        for(WuGui wuGui : wuGuis){
            System.out.println(wuGui);
        }
    }
}

// 乌龟
class WuGui{

    int age;

    public WuGui(int age){
        this.age = age;
    }

    @Override
    public String toString() {
        return "小乌龟[" +
                "age=" + age +
                ']';
    }
}

// 单独在这里编写一个比较器
// 比较器实现java.util.Comparator接口。(Comparable是java.lang包下的。Comparator是java.util包下的。)
/*
class WuGuiComparator implements Comparator<WuGui> {

    @Override
    public int compare(WuGui o1, WuGui o2) {
        // 指定比较规则
        // 按照年龄排序
        return o1.age - o2.age;
    }
}
 */

二叉树

erchashutu

Collections 工具类

import java.util.*;

/*
java.util.Collection 集合接口
java.util.Collections 集合工具类,方便集合的操作。
 */
public class CollectionsTest {
    public static void main(String[] args) {

        // ArrayList集合不是线程安全的。
        List<String> list = new ArrayList<>();

        // 变成线程安全的
        Collections.synchronizedList(list);

        // 排序
        list.add("abf");
        list.add("abx");
        list.add("abc");
        list.add("abe");

        Collections.sort(list);
        for(String s : list){
            System.out.println(s);
        }

        List<WuGui2> wuGuis = new ArrayList<>();
        wuGuis.add(new WuGui2(1000));
        wuGuis.add(new WuGui2(8000));
        wuGuis.add(new WuGui2(500));
        // 注意:对List集合中元素排序,需要保证List集合中的元素实现了:Comparable接口。
        Collections.sort(wuGuis);
        for(WuGui2 wg : wuGuis){
            System.out.println(wg);
        }

        // 对Set集合怎么排序呢?
        Set<String> set = new HashSet<>();
        set.add("king");
        set.add("kingsoft");
        set.add("king2");
        set.add("king1");
        // 将Set集合转换成List集合
        List<String> myList = new ArrayList<>(set);
        Collections.sort(myList);
        for(String s : myList) {
            System.out.println(s);
        }

        // 这种方式也可以排序。
        //Collections.sort(list集合, 比较器对象);
    }
}

class WuGui2 implements Comparable<WuGui2>{
    int age;
    public WuGui2(int age){
        this.age = age;
    }

    @Override
    public int compareTo(WuGui2 o) {
        return this.age - o.age;
    }

    @Override
    public String toString() {
        return "WuGui2{" +
                "age=" + age +
                '}';
    }
}

IO

  • I : Input
  • O : Output
  • 通过 IO 可以完成硬盘文件的读和写。
  • 什么是io

IO 流分类

  • 一种方式是按照流的方向进行分类:

    • 以内存作为参照物
    • 往内存中去,叫做输入(Input)。或者叫做读(Read) 输入流
    • 从内存中出来,叫做输出(Output)。或者叫做写(Write) 输出流
  • 另一种方式是按照读取数据方式不同进行分类:

    • 有的流是按照字节的方式读取数据,一次读取 1 个字节 byte,等同于一次读取 8 个二进制位
    • 这种流是万能的,什么类型的文件都可以读取。包括:文本文件,图片,声音文件,视频文件等....
    • 假设文件 file1.txt,采用字节流的话是这样读的: a 中国 bc 张三 fe 第一次读:一个字节,正好读到'a' 第二次读:一个字节,正好读到'中'字符的一半。 第三次读:一个字节,正好读到'中'字符的另外一半。
  • 有的流是按照字符的方式读取数据的,一次读取一个字符,这种流是为了方便读取,普通文本文件而存在的,这种流不能读取:图片、声音、视频等文件。只能读取纯

    • 文本文件,连 word 文件都无法读取。 假设文件 file1.txt,采用字符流的话是这样读的: a 中国 bc 张三 fe 第一次读:'a'字符('a'字符在 windows 系统中占用 1 个字节。) 第二次读:'中'字符('中'字符在 windows 系统中占用 2 个字节。)
  • 综上所述:流的分类

    • 输入流、输出流
    • 字节流、字符流

java IO 流四个主要类

  • java.io.InputStream 字节输入流
  • java.io.OutputStream 字节输出流
  • java.io.Reader 字符输入流
  • java.io.Writer 字符输出流
  • 都是抽象类。(abstract class)
  • 所有的流都实现了:
    • java.io.Closeable 接口,都是可关闭的,都有 close()方法。
    • 流毕竟是一个管道,这个是内存和硬盘之间的通道,用完之后一定要关闭,不然会耗费(占用)很多资源。养成好习惯,用完流一定要关闭。
  • 所有的输出流都实现了:
    • java.io.Flushable 接口,都是可刷新的,都有 flush()方法。
    • 养成一个好习惯,输出流在最终输出之后,一定要记得 flush()刷新一下。这个刷新表示将通道/管道当中剩余未输出的数据强行输出完(清空管道!)刷新的作用就是清空管道。
    • 注意:如果没有 flush()可能会导致丢失数据。

注意:在 java 中只要“类名”以 Stream 结尾的都是字节流。以“Reader/Writer”结尾的都是字符流。

FileInputStream

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

/*
最终版,需要掌握。
 */
public class FileInputStreamTest04 {
    public static void main(String[] args) {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream("chapter23/src/tempfile3");
            // 准备一个byte数组
            byte[] bytes = new byte[4];
            /*while(true){
                int readCount = fis.read(bytes);
                if(readCount == -1){
                    break;
                }
                // 把byte数组转换成字符串,读到多少个转换多少个。
                System.out.print(new String(bytes, 0, readCount));
            }*/

            int readCount = 0;
            while ((readCount = fis.read(bytes)) != -1) {
                System.out.print(new String(bytes, 0, readCount));
            }

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

/*
FileInputStream类的其它常用方法:
    int available():返回流当中剩余的没有读到的字节数量
    long skip(long n):跳过几个字节不读。
 */
public class FileInputStreamTest05 {
    public static void main(String[] args) {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream("tempfile");
            System.out.println("总字节数量:" + fis.available());
            // 读1个字节
            //int readByte = fis.read();
            // 还剩下可以读的字节数量是:5
            //System.out.println("剩下多少个字节没有读:" + fis.available());
            // 这个方法有什么用?
            //byte[] bytes = new byte[fis.available()]; // 这种方式不太适合太大的文件,因为byte[]数组不能太大。
            // 不需要循环了。
            // 直接读一次就行了。
            //int readCount = fis.read(bytes); // 6
            //System.out.println(new String(bytes)); // abcdef

            // skip跳过几个字节不读取,这个方法也可能以后会用!
            fis.skip(3);
            System.out.println(fis.read()); //100

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

FileOutputStream

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * 文件字节输出流,负责写。
 * 从内存到硬盘。
 */
public class FileOutputStreamTest01 {
    public static void main(String[] args) {
        FileOutputStream fos = null;
        try {
            // myfile文件不存在的时候会自动新建!
            // 这种方式谨慎使用,这种方式会先将原文件清空,然后重新写入。
            //fos = new FileOutputStream("myfile");
            //fos = new FileOutputStream("chapter23/src/tempfile3");

            // 以追加的方式在文件末尾写入。不会清空原文件内容。
            fos = new FileOutputStream("chapter23/src/tempfile3", true);
            // 开始写。
            byte[] bytes = {97, 98, 99, 100};
            // 将byte数组全部写出!
            fos.write(bytes); // abcd
            // 将byte数组的一部分写出!
            fos.write(bytes, 0, 2); // 再写出ab

            // 字符串
            String s = "我是一个中国人,我骄傲!!!";
            // 将字符串转换成byte数组。
            byte[] bs = s.getBytes();
            // 写
            fos.write(bs);

            // 写完之后,最后一定要刷新
            fos.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

FileInputStream 文件拷贝

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class copy01 {
    /*
    使用FileInputStream + FileOutputStream完成文件的拷贝。
    拷贝的过程应该是一边读,一边写。
    使用以上的字节流拷贝文件的时候,文件类型随意,万能的。什么样的文件都能拷贝。
     */
    public static void main(String[] args) {
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            // 创建一个输入流对象
            fis = new FileInputStream("C:\\Users\\ita20\\Desktop\\demo\\demo.css");
            // 创建一个输出流对象
            fos = new FileOutputStream("C:\\Users\\ita20\\Desktop\\demo.css");

            // 最核心的:一边读,一边写
            byte[] bytes = new byte[1024 * 1024]; // 1MB(一次最多拷贝1MB。)
            int readCount = 0;
            while ((readCount = fis.read(bytes)) != -1) {
                fos.write(bytes, 0, readCount);
            }

            // 刷新,输出流最后要刷新
            fos.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 分开try,不要一起try。
            // 一起try的时候,其中一个出现异常,可能会影响到另一个流的关闭。
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

FileReader

/*
FileReader:
    文件字符输入流,只能读取普通文本。
    读取文本内容时,比较方便,快捷。
 */
public class FileReaderTest {
    public static void main(String[] args) {
        FileReader reader = null;
        try {
            // 创建文件字符输入流
            reader = new FileReader("tempfile");

            //准备一个char数组
            char[] chars = new char[4];
            // 往char数组中读
            reader.read(chars); // 按照字符的方式读取:第一次e,第二次f,第三次 风....
            for(char c : chars) {
                System.out.println(c);
            }

            /*// 开始读
            char[] chars = new char[4]; // 一次读取4个字符
            int readCount = 0;
            while((readCount = reader.read(chars)) != -1) {
                System.out.print(new String(chars,0,readCount));
            }*/
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

FileReader 文件拷贝

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

/*
使用FileReader FileWriter进行拷贝的话,只能拷贝“普通文本”文件。
 */
public class Copy02 {
    public static void main(String[] args) {
        FileReader in = null;
        FileWriter out = null;
        try {
            // 读
            in = new FileReader("chapter23/src/com/bjpowernode/java/io/Copy02.java");
            // 写
            out = new FileWriter("C111111.java");

            // 一边读一边写:
            char[] chars = new char[1024 * 512]; // 1MB
            int readCount = 0;
            while((readCount = in.read(chars)) != -1){
                out.write(chars, 0, readCount);
            }

            // 刷新
            out.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

BufferedReader

import java.io.BufferedReader;
import java.io.FileReader;

/*
BufferedReader:
    带有缓冲区的字符输入流。
    使用这个流的时候不需要自定义char数组,或者说不需要自定义byte数组。自带缓冲。
 */
public class BufferedReaderTest01 {
    public static void main(String[] args) throws Exception{

        FileReader reader = new FileReader("Copy02.java");
        // 当一个流的构造方法中需要一个流的时候,这个被传进来的流叫做:节点流。
        // 外部负责包装的这个流,叫做:包装流,还有一个名字叫做:处理流。
        // 像当前这个程序来说:FileReader就是一个节点流。BufferedReader就是包装流/处理流。
        BufferedReader br = new BufferedReader(reader);

        // 读一行
        /*String firstLine = br.readLine();
        System.out.println(firstLine);

        String secondLine = br.readLine();
        System.out.println(secondLine);

        String line3 = br.readLine();
        System.out.println(line3);*/

        // br.readLine()方法读取一个文本行,但不带换行符。
        String s = null;
        while((s = br.readLine()) != null){
            System.out.print(s);
        }

        // 关闭流
        // 对于包装流来说,只需要关闭最外层流就行,里面的节点流会自动关闭。(可以看源代码。)
        br.close();
    }
}

InputStreamReader

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;

/*
    转换流:InputStreamReader
 */
public class BufferedReaderTest02 {
    public static void main(String[] args) throws Exception{

        /*// 字节流
        FileInputStream in = new FileInputStream("Copy02.java");

        // 通过转换流转换(InputStreamReader将字节流转换成字符流。)
        // in是节点流。reader是包装流。
        InputStreamReader reader = new InputStreamReader(in);

        // 这个构造方法只能传一个字符流。不能传字节流。
        // reader是节点流。br是包装流。
        BufferedReader br = new BufferedReader(reader);*/

        // 合并
        BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("Copy02.java")));

        String line = null;
        while((line = br.readLine()) != null){
            System.out.println(line);
        }

        // 关闭最外层
        br.close();
    }
}

BufferedWriter

import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.OutputStreamWriter;

/*
BufferedWriter:带有缓冲的字符输出流。
OutputStreamWriter:转换流
 */
public class BufferedWriterTest {
    public static void main(String[] args) throws Exception{
        // 带有缓冲区的字符输出流
        //BufferedWriter out = new BufferedWriter(new FileWriter("copy"));

        BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("copy.js", true)));
        // 开始写。
        out.write("hello world!");
        out.write("\n");
        out.write("hello kitty!");
        // 刷新
        out.flush();
        // 关闭最外层
        out.close();
    }
}

DataInputStream(读)

import java.io.DataInputStream;
import java.io.FileInputStream;

/*
DataInputStream:数据字节输入流。
DataOutputStream写的文件,只能使用DataInputStream去读。并且读的时候你需要提前知道写入的顺序。
读的顺序需要和写的顺序一致。才可以正常取出数据。

 */
public class DataInputStreamTest01 {
    public static void main(String[] args) throws Exception{
        DataInputStream dis = new DataInputStream(new FileInputStream("data"));
        // 开始读
        byte b = dis.readByte();
        short s = dis.readShort();
        int i = dis.readInt();
        long l = dis.readLong();
        float f = dis.readFloat();
        double d = dis.readDouble();
        boolean sex = dis.readBoolean();
        char c = dis.readChar();

        System.out.println(b);
        System.out.println(s);
        System.out.println(i + 1000);
        System.out.println(l);
        System.out.println(f);
        System.out.println(d);
        System.out.println(sex);
        System.out.println(c);

        dis.close();
    }
}

DataOutputStream(写)

import java.io.DataOutputStream;
import java.io.FileOutputStream;

/*
java.io.DataOutputStream:数据专属的流。
这个流可以将数据连同数据的类型一并写入文件。
注意:这个文件不是普通文本文档。(这个文件使用记事本打不开。)
 */
public class DataOutputStreamTest {
    public static void main(String[] args) throws Exception{
        // 创建数据专属的字节输出流
        DataOutputStream dos = new DataOutputStream(new FileOutputStream("data"));
        // 写数据
        byte b = 100;
        short s = 200;
        int i = 300;
        long l = 400L;
        float f = 3.0F;
        double d = 3.14;
        boolean sex = false;
        char c = 'a';
        // 写
        dos.writeByte(b); // 把数据以及数据的类型一并写入到文件当中。
        dos.writeShort(s);
        dos.writeInt(i);
        dos.writeLong(l);
        dos.writeFloat(f);
        dos.writeDouble(d);
        dos.writeBoolean(sex);
        dos.writeChar(c);

        // 刷新
        dos.flush();
        // 关闭最外层
        dos.close();
    }
}

PrintStream

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.text.SimpleDateFormat;
import java.util.Date;

public class LogPrint {
    public static void log(String msg) {
        try {
            // 指向一个日志文件                     将文件对象转换
            PrintStream printStream = new PrintStream(new FileOutputStream("C:\\Users\\ita20\\Desktop\\demo\\log.txt", true));

            //改变输出方向
            System.setOut(printStream);

            //获取当前时间
            Date date = new Date();
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
            String dtime = simpleDateFormat.format(date);

            //将日志输入
            System.out.println(dtime + ":" + msg);

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

    }
}

File

/*
File
    1、File类和四大家族没有关系,所以File类不能完成文件的读和写。
    2、File对象代表什么?
        文件和目录路径名的抽象表示形式。
        C:\Drivers 这是一个File对象
        C:\Drivers\Lan\Realtek\Readme.txt 也是File对象。
        一个File对象有可能对应的是目录,也可能是文件。
        File只是一个路径名的抽象表示形式。
    3、需要掌握File类中常用的方法
 */
public class FileTest01 {
    public static void main(String[] args) throws Exception {
        // 创建一个File对象
        File f1 = new File("D:\\file");

        // 判断是否存在!
        System.out.println(f1.exists());

        // 如果D:\file不存在,则以文件的形式创建出来
        /*if(!f1.exists()) {
            // 以文件形式新建
            f1.createNewFile();
        }*/

        // 如果D:\file不存在,则以目录的形式创建出来
        /*if(!f1.exists()) {
            // 以目录的形式新建。
            f1.mkdir();
        }*/

        // 可以创建多重目录吗?
        File f2 = new File("D:/a/b/c/d/e/f");
        /*if(!f2.exists()) {
            // 多重目录的形式新建。
            f2.mkdirs();
        }*/

        File f3 = new File("D:\\course\\01-开课\\学习方法.txt");
        // 获取文件的父路径
        String parentPath = f3.getParent();
        System.out.println(parentPath); //D:\course\01-开课
        File parentFile = f3.getParentFile();
        System.out.println("获取绝对路径:" + parentFile.getAbsolutePath());

        File f4 = new File("copy");
        System.out.println("绝对路径:" + f4.getAbsolutePath()); // C:\Users\Administrator\IdeaProjects\javase\copy

    }
}
/*
File类的常用方法
 */
public class FileTest02 {
    public static void main(String[] args) {

        File f1 = new File("D:\\course\\01-开课\\开学典礼.ppt");
        // 获取文件名
        System.out.println("文件名:" + f1.getName());

        // 判断是否是一个目录
        System.out.println(f1.isDirectory()); // false

        // 判断是否是一个文件
        System.out.println(f1.isFile()); // true

        // 获取文件最后一次修改时间
        long haoMiao = f1.lastModified(); // 这个毫秒是从1970年到现在的总毫秒数。
        // 将总毫秒数转换成日期?????
        Date time = new Date(haoMiao);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
        String strTime = sdf.format(time);
        System.out.println(strTime);

        // 获取文件大小
        System.out.println(f1.length()); //216064字节。
    }
}

拷贝文件目录

public class Test01 {
    public static void main(String[] args) {
//        File file = new File("E:\\002-JavaSE每章作业");
//
//        for (File fs : file.listFiles()) {
//            System.out.println(fs.getName());
//        }
        Readwrite(new File("C:\\Users\\ita20\\Desktop\\被拷贝文件\\"), new File("C:\\Users\\ita20\\Desktop\\拷贝到此处\\"));
    }

    public static void Readwrite(File filePath, File nfilePaath) {
        if (filePath.isFile()) { //是文件
            FileOutputStream fout = null;//写
            FileInputStream finp = null;//读

            try {
                //实例需要拷贝的路径
                finp = new FileInputStream(filePath);
                String path = nfilePaath.getAbsolutePath() + "\\" + filePath.getName();

                fout = new FileOutputStream(path);

                byte[] bytes = new byte[1024 * 1024];

                int i = 0;//每次拷贝的字节数
                while ((i = finp.read(bytes)) != -1) {
                    //写入到文件中
                    fout.write(bytes);
                }

                fout.flush();//写入通道每次刷新
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (finp == null) {
                    try {
                        finp.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (fout == null) {
                    try {
                        fout.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            return;
        }
        File[] fs = filePath.listFiles();
        for (File file : fs) {
            if (file.isDirectory()) {
                String path = nfilePaath.getAbsolutePath() + "\\" + file.getName();
                File ff = new File(path);
                if (!ff.exists()) {
                    ff.mkdirs();
                    Readwrite(file, ff);
                    System.out.println(ff);
                }
            } else {
                Readwrite(file, nfilePaath);
            }
        }
    }

    /**
     * 拷贝目录
     *
     * @param srcFile  拷贝源
     * @param destFile 拷贝目标
     */
    private static void copyDir(File srcFile, File destFile) {
        if (srcFile.isFile()) {
            // srcFile如果是一个文件的话,递归结束。
            // 是文件的时候需要拷贝。
            // ....一边读一边写。
            FileInputStream in = null;
            FileOutputStream out = null;
            try {
                // 读这个文件
                // D:\course\02-JavaSE\document\JavaSE进阶讲义\JavaSE进阶-01-面向对象.pdf
                in = new FileInputStream(srcFile);
                // 写到这个文件中
                // C:\course\02-JavaSE\document\JavaSE进阶讲义\JavaSE进阶-01-面向对象.pdf
                String path = (destFile.getAbsolutePath().endsWith("\\") ? destFile.getAbsolutePath() : destFile.getAbsolutePath() + "\\") + srcFile.getAbsolutePath().substring(3);
                out = new FileOutputStream(path);
                // 一边读一边写
                byte[] bytes = new byte[1024 * 1024]; // 一次复制1MB
                int readCount = 0;
                while ((readCount = in.read(bytes)) != -1) {
                    out.write(bytes, 0, readCount);
                }
                out.flush();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (out != null) {
                    try {
                        out.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                if (in != null) {
                    try {
                        in.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            return;
        }
        // 获取源下面的子目录
        File[] files = srcFile.listFiles();
        for (File file : files) {
            // 获取所有文件的(包括目录和文件)绝对路径
            //System.out.println(file.getAbsolutePath());
            if (file.isDirectory()) {
                // 新建对应的目录
                //System.out.println(file.getAbsolutePath());
                //D:\course\02-JavaSE\document\JavaSE进阶讲义       源目录
                //C:\course\02-JavaSE\document\JavaSE进阶讲义       目标目录
                String srcDir = file.getAbsolutePath();
                String destDir = (destFile.getAbsolutePath().endsWith("\\") ? destFile.getAbsolutePath() : destFile.getAbsolutePath() + "\\") + srcDir.substring(3);
                File newFile = new File(destDir);
                if (!newFile.exists()) {
                    newFile.mkdirs();

                    System.out.println(newFile);
                }
            }
            // 递归调用
            System.out.println(destFile + ",,,,destFile");
            copyDir(file, destFile);
        }
    }
}

序列化\反序列化

import java.io.Serializable;

public class Student implements Serializable {

    // IDEA工具自动生成序列化版本号。
    //private static final long serialVersionUID = -7998917368642754840L;

    // Java虚拟机看到Serializable接口之后,会自动生成一个序列化版本号。
    // 这里没有手动写出来,java虚拟机会默认提供这个序列化版本号。
    // 建议将序列化版本号手动的写出来。不建议自动生成
    private static final long serialVersionUID = 1L; // java虚拟机识别一个类的时候先通过类名,如果类名一致,再通过序列化版本号。

    private int no;
    //private String name;

    // 过了很久,Student这个类源代码改动了。
    // 源代码改动之后,需要重新编译,编译之后生成了全新的字节码文件。
    // 并且class文件再次运行的时候,java虚拟机生成的序列化版本号也会发生相应的改变。
    private int age;
    private String email;
    // transient关键字表示游离的,不参与序列化。
    private transient String address;//address不参与序列化操作

    public Student() {
    }

    public Student(int no, String name) {
        this.no = no;
        //this.name = name;
    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    /*public String getName() {
        return name;
    }*/

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

    /*@Override
    public String toString() {
        return "Student{" +
                "no=" + no +
                ", name='" + name + '\'' +
                '}';
    }*/

    @Override
    public String toString() {
        return "Student{" +
                "no=" + no +
                ", age=" + age +
                ", email='" + email + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
}

序列化写入

/*
一次序列化多个对象呢?
    可以,可以将对象放到集合当中,序列化集合。
提示:
    参与序列化的ArrayList集合以及集合中的元素User都需要实现 java.io.Serializable接口。
 */
public class ObjectOutputStreamTest02 {
    public static void main(String[] args) throws Exception{
        List<Student> userList = new ArrayList<>();
        userList.add(new Student(1,"zhangsan"));
        userList.add(new Student(2, "lisi"));
        userList.add(new Student(3, "wangwu"));
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Student"));

        // 序列化一个集合,这个集合对象中放了很多其他对象。
        oos.writeObject(userList);

        oos.flush();
        oos.close();
    }
}

反序列化读取

/*
反序列化集合
 */
public class ObjectInputStreamTest02 {
    public static void main(String[] args) throws Exception{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("Student"));
        //Object obj = ois.readObject();
        //System.out.println(obj instanceof List);
        List<Student> userList = (List<Student>)ois.readObject();
        for(Student user : userList){
            System.out.println(user);
        }
        ois.close();
    }
}

I0+Properties

import java.io.FileReader;
import java.util.Properties;

/*
IO+Properties的联合应用。
非常好的一个设计理念:
    以后经常改变的数据,可以单独写到一个文件中,使用程序动态读取。
    将来只需要修改这个文件的内容,java代码不需要改动,不需要重新
    编译,服务器也不需要重启。就可以拿到动态的信息。

    类似于以上机制的这种文件被称为配置文件。
    并且当配置文件中的内容格式是:
        key1=value
        key2=value
    的时候,我们把这种配置文件叫做属性配置文件。

    java规范中有要求:属性配置文件建议以.properties结尾,但这不是必须的。
    这种以.properties结尾的文件在java中被称为:属性配置文件。
    其中Properties是专门存放属性配置文件内容的一个类。
 */
public class IoPropertiesTest01 {
    public static void main(String[] args) throws Exception{
        /*
        Properties是一个Map集合,key和value都是String类型。
        想将userinfo文件中的数据加载到Properties对象当中。
         */
        // 新建一个输入流对象
        FileReader reader = new FileReader("chapter23/userinfo.properties");

        // 新建一个Map集合
        Properties pro = new Properties();

        // 调用Properties对象的load方法将文件中的数据加载到Map集合中。
        pro.load(reader); // 文件中的数据顺着管道加载到Map集合中,其中等号=左边做key,右边做value

        // 通过key来获取value呢?
        String username = pro.getProperty("username");
        System.out.println(username);

        String password = pro.getProperty("password");
        System.out.println(password);

        String data = pro.getProperty("data");
        System.out.println(data);

        String usernamex = pro.getProperty("usernamex");
        System.out.println(usernamex);
    }
}

*流

文件专属: java.io.FileInputStream(掌握) java.io.FileOutputStream(掌握) java.io.FileReader java.io.FileWriter 转换流:(将字节流转换成字符流) java.io.InputStreamReader java.io.OutputStreamWriter

缓冲流专属: java.io.BufferedReader java.io.BufferedWriter java.io.BufferedInputStream java.io.BufferedOutputStream

数据流专属: java.io.DataInputStream java.io.DataOutputStream

标准输出流: java.io.PrintWriter java.io.PrintStream(掌握)

对象专属流: java.io.ObjectInputStream(掌握) java.io.ObjectOutputStream(掌握)

Thread

什么是进程?什么是线程?

  • 进程是一个应用程序(1 个进程是一个软件)。

  • 线程是一个进程中的执行场景/执行单元。

  • 一个进程可以启动多个线程。

  • 对于 java 程序来说,当在 DOS 命令窗口中输入:

    • java HelloWorld 回车之后。
    • 会先启动 JVM,而 JVM 就是一个进程。
    • JVM 再启动一个主线程调用 main 方法。
    • 同时再启动一个垃圾回收线程负责看护,回收垃圾。
    • 最起码,现在的 java 程序中至少有两个线程并发,一个是垃圾回收线程,一个是执行 main 方法的主线程。
  • 举个例子

    • 阿里巴巴:进程
      		马云:阿里巴巴的一个线程
      		童文红:阿里巴巴的一个线程
      京东:进程
      	强东:京东的一个线程
      	妹妹:京东的一个线程
      
      进程可以看做是现实生活当中的公司。
      线程可以看做是公司当中的某个员工。
      
      注意:
      	进程A和进程B的内存独立不共享。(阿里巴巴和京东资源不会共享的!)
      		魔兽游戏是一个进程
      		酷狗音乐是一个进程
      		这两个进程是独立的,不共享资源。
      
      	线程A和线程B呢?
      		在java语言中:
      			线程A和线程B,堆内存和方法区内存共享。
      			但是栈内存独立,一个线程一个栈。
      
      		假设启动10个线程,会有10个栈空间,每个栈和每个栈之间,
      		互不干扰,各自执行各自的,这就是多线程并发。
      
      	火车站,可以看做是一个进程。
      	火车站中的每一个售票窗口可以看做是一个线程。
      	我在窗口1购票,你可以在窗口2购票,你不需要等我,我也不需要等你。
      	所以多线程并发可以提高效率。
      
      	java中之所以有多线程机制,目的就是为了提高程序的处理效率。
      
  • 使用了多线程机制之后,main 方法结束,是不是有可能程序也不会结束。main 方法结束只是主线程结束了,主栈空了,其它的栈(线程)可能还在压栈弹栈。

  • 分析一个问题:对于单核的 CPU 来说,真的可以做到真正的多线程并发吗?

    对于多核的CPU电脑来说,真正的多线程并发是没问题的。 4核CPU表示同一个时间点上,可以真正的有4个进程并发执行。

    什么是真正的多线程并发? t1线程执行t1的。 t2线程执行t2的。 t1不会影响t2,t2也不会影响t1。这叫做真正的多线程并发。

    单核的CPU表示只有一个大脑: 不能够做到真正的多线程并发,但是可以做到给人一种“多线程并发”的感觉。 对于单核的CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于 CPU的处理速度极快,多个线程之间频繁切换执行,跟人来的感觉是:多个事情 同时在做!!!!! 线程A:播放音乐 线程B:运行魔兽游戏 线程A和线程B频繁切换执行,人类会感觉音乐一直在播放,游戏一直在运行, 给我们的感觉是同时并发的。

    电影院采用胶卷播放电影,一个胶卷一个胶卷播放速度达到一定程度之后, 人类的眼睛产生了错觉,感觉是动画的。这说明人类的反应速度很慢,就像 一根钢针扎到手上,到最终感觉到疼,这个过程是需要“很长的”时间的,在 这个期间计算机可以进行亿万次的循环。所以计算机的执行速度很快。

  • 一个线程一个栈

    • 进程中方法区和堆区是公用的
    • 一个线程一个栈

实现线程的两种方式

java 支持多线程机制。并且 java 已经将多线程实现了,我们只需要继承就行了。

第一种方式:编写一个类,直接继承java.lang.Thread,重写run方法。
	// 定义线程类
	public class MyThread extends Thread{
		public void run(){

		}
	}
	// 创建线程对象
	MyThread t = new MyThread();
	// 启动线程。
	t.start();

第二种方式:编写一个类,实现java.lang.Runnable接口,实现run方法。
	// 定义一个可运行的类
	public class MyRunnable implements Runnable {
		public void run(){

		}
	}
	// 创建线程对象
	Thread t = new Thread(new MyRunnable());
	// 启动线程
	t.start();

注意:第二种方式实现接口比较常用,因为一个类实现了接口,它还可以去继承
其它的类,更灵活。

Start/Run

Run

![线程run方法](https://iskr.gitee.io/pic/blog/线程run方法 .png)

Start

线程start方法

线程的生命周期

线程的生命周期

getName

  • 获得线程名字

    • /*
      1、怎么获取当前线程对象?
          Thread t = Thread.currentThread();
          返回值t就是当前线程。
      
      2、获取线程对象的名字
          String name = 线程对象.getName();
      
      3、修改线程对象的名字
          线程对象.setName("线程名字");
      
      4、当线程没有设置名字的时候,默认的名字有什么规律?(了解一下)
          Thread-0
          Thread-1
          Thread-2
          Thread-3
          .....
       */
      public class ThreadTest05 {
          public void doSome(){
              // 这样就不行了
              //this.getName();
              //super.getName();
              // 但是这样可以
              String name = Thread.currentThread().getName();
              System.out.println("------->" + name);
          }
      
          public static void main(String[] args) {
              ThreadTest05 tt = new ThreadTest05();
              tt.doSome();
      
              //currentThread就是当前线程对象
              // 这个代码出现在main方法当中,所以当前线程就是主线程。
              Thread currentThread = Thread.currentThread();
              System.out.println(currentThread.getName()); //main
      
              // 创建线程对象
              MyThread2 t = new MyThread2();
              // 设置线程的名字
              t.setName("t1");
              // 获取线程的名字
              String tName = t.getName();
              System.out.println(tName); //Thread-0
      
              MyThread2 t2 = new MyThread2();
              t2.setName("t2");
              System.out.println(t2.getName()); //Thread-1\
              t2.start();
      
              // 启动线程
              t.start();
          }
      }
      
      class MyThread2 extends Thread {
          public void run(){
              for(int i = 0; i < 100; i++){
                  // currentThread就是当前线程对象。当前线程是谁呢?
                  // 当t1线程执行run方法,那么这个当前线程就是t1
                  // 当t2线程执行run方法,那么这个当前线程就是t2
                  Thread currentThread = Thread.currentThread();
                  System.out.println(currentThread.getName() + "-->" + i);
      
                  //System.out.println(super.getName() + "-->" + i);
                  //System.out.println(this.getName() + "-->" + i);
              }
          }
      }
      

Sleep

  • /*
    关于线程的sleep方法:
        static void sleep(long millis)
        1、静态方法:Thread.sleep(1000);
        2、参数是毫秒
        3、作用:让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让给其它线程使用。
            这行代码出现在A线程中,A线程就会进入休眠。
            这行代码出现在B线程中,B线程就会进入休眠。
        4、Thread.sleep()方法,可以做到这种效果:
            间隔特定的时间,去执行一段特定的代码,每隔多久执行一次。
     */
    public class ThreadTest06 {
        public static void main(String[] args) {
    
            // 让当前线程进入休眠,睡眠5秒
            // 当前线程是主线程!!!
            /*try {
                Thread.sleep(1000 * 5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
    
            // 5秒之后执行这里的代码
            //System.out.println("hello world!");
    
            for(int i = 0; i < 10; i++){
                System.out.println(Thread.currentThread().getName() + "--->" + i);
    
                // 睡眠1秒
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
  • Sleep 面试题

    • Sleep 是静态方法,和对象无关

    • /*
      关于Thread.sleep()方法的一个面试题:
       */
      public class ThreadTest07 {
          public static void main(String[] args) {
              // 创建线程对象
              Thread t = new MyThread3();
              t.setName("t");
              t.start();
      
              // 调用sleep方法
              try {
                  // 问题:这行代码会让线程t进入休眠状态吗?
                  t.sleep(1000 * 5); // 在执行的时候还是会转换成:Thread.sleep(1000 * 5);
                                           // 这行代码的作用是:让当前线程进入休眠,也就是说main线程进入休眠。
                                           // 这样代码出现在main方法中,main线程睡眠。
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
      
              // 5秒之后这里才会执行。
              System.out.println("hello World!");
          }
      }
      
      class MyThread3 extends Thread {
          public void run(){
              for(int i = 0; i < 10000; i++){
                  System.out.println(Thread.currentThread().getName() + "--->" + i);
              }
          }
      }
      
  • 中止睡眠的线程interrupt

    • /*
      sleep睡眠太久了,如果希望半道上醒来,你应该怎么办?也就是说怎么叫醒一个正在睡眠的线程??
          注意:这个不是终断线程的执行,是终止线程的睡眠。
       */
      public class ThreadTest08 {
          public static void main(String[] args) {
              Thread t = new Thread(new MyRunnable2());
              t.setName("t");
              t.start();
      
              // 希望5秒之后,t线程醒来(5秒之后主线程手里的活儿干完了。)
              try {
                  Thread.sleep(1000 * 5);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              // 终断t线程的睡眠(这种终断睡眠的方式依靠了java的异常处理机制。)
              t.interrupt(); // 干扰,一盆冷水过去!
          }
      }
      
      class MyRunnable2 implements Runnable {
      
          // 重点:run()当中的异常不能throws,只能try catch
          // 因为run()方法在父类中没有抛出任何异常,子类不能比父类抛出更多的异常。
          @Override
          public void run() {
              System.out.println(Thread.currentThread().getName() + "---> begin");
              try {
                  // 睡眠1年
                  Thread.sleep(1000 * 60 * 60 * 24 * 365);
              } catch (InterruptedException e) {
                  // 打印异常信息
                  //e.printStackTrace();
              }
              //1年之后才会执行这里
              System.out.println(Thread.currentThread().getName() + "---> end");
      
              // 调用doOther
              //doOther();
          }
      
          // 其它方法可以throws
          /*public void doOther() throws Exception{
      
          }*/
      }
      

Stop

  • 这种方式存在很大的缺点:容易丢失数据。因为这种方式是直接将线程杀死了

  • /*
    在java中怎么强行终止一个线程的执行。
        这种方式存在很大的缺点:容易丢失数据。因为这种方式是直接将线程杀死了,
        线程没有保存的数据将会丢失。不建议使用。
     */
    public class ThreadTest09 {
        public static void main(String[] args) {
            Thread t = new Thread(new MyRunnable3());
            t.setName("t");
            t.start();
    
            // 模拟5秒
            try {
                Thread.sleep(1000 * 5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 5秒之后强行终止t线程
            t.stop(); // 已过时(不建议使用。)
        }
    }
    
    class MyRunnable3 implements Runnable {
    
        @Override
        public void run() {
            for(int i = 0; i < 10; i++){
                System.out.println(Thread.currentThread().getName() + "--->" + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

终止线程

  • 给定一个条件去判断是否执行线程,若为 false 则直接 retuen 终止 run 方法结束线程

  • /*
    怎么合理的终止一个线程的执行。这种方式是很常用的。
     */
    public class ThreadTest10 {
        public static void main(String[] args) {
            MyRunable4 r = new MyRunable4();
            Thread t = new Thread(r);
            t.setName("t");
            t.start();
    
            // 模拟5秒
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 终止线程
            // 你想要什么时候终止t的执行,那么你把标记修改为false,就结束了。
            r.run = false;
        }
    }
    
    class MyRunable4 implements Runnable {
    
        // 打一个布尔标记
        boolean run = true;
    
        @Override
        public void run() {
            for (int i = 0; i < 10; i++){
                if(run){
                    System.out.println(Thread.currentThread().getName() + "--->" + i);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else{
                    // return就结束了,你在结束之前还有什么没保存的。
                    // 在这里可以保存呀。
                    //save....
    
                    //终止当前线程
                    return;
                }
            }
        }
    }
    

线程优先级 setPriority

  • 线程优先级越高,抢占的 CPU 时间片就相对更多,几率更大。

  • /*
    了解:关于线程的优先级
     */
    public class ThreadTest11 {
        public static void main(String[] args) {
            // 设置主线程的优先级为1
            Thread.currentThread().setPriority(1);
    
            /*System.out.println(&quot;最高优先级&quot; + Thread.MAX_PRIORITY);
            System.out.println(&quot;最低优先级&quot; + Thread.MIN_PRIORITY);
            System.out.println(&quot;默认优先级&quot; + Thread.NORM_PRIORITY);*/
    
            // 获取当前线程对象,获取当前线程的优先级
            Thread currentThread = Thread.currentThread();
            // main线程的默认优先级是:5
            //System.out.println(currentThread.getName() + &quot;线程的默认优先级是:&quot; + currentThread.getPriority());
    
            Thread t = new Thread(new MyRunnable5());
            t.setPriority(10);
            t.setName(&quot;t&quot;);
            t.start();
    
            // 优先级较高的,只是抢到的CPU时间片相对多一些。
            // 大概率方向更偏向于优先级比较高的。
            for(int i = 0; i &lt; 10000; i++){
                System.out.println(Thread.currentThread().getName() + &quot;--&gt;&quot; + i);
            }
    
    
        }
    }
    
    class MyRunnable5 implements Runnable {
    
        @Override
        public void run() {
            // 获取线程优先级
            //System.out.println(Thread.currentThread().getName() + &quot;线程的默认优先级:&quot; + Thread.currentThread().getPriority());
            for(int i = 0; i &lt; 10000; i++){
                System.out.println(Thread.currentThread().getName() + &quot;--&gt;&quot; + i);
            }
        }
    }xia
    

线程让位 yield

  • 给点一定的条件让出线程的执行权

  • /*
    让位,当前线程暂停,回到就绪状态,让给其它线程。
    静态方法:Thread.yield();
     */
    public class ThreadTest12 {
        public static void main(String[] args) {
            Thread t = new Thread(new MyRunnable6());
            t.setName("t");
            t.start();
    
            for(int i = 1; i <= 10000; i++) {
                System.out.println(Thread.currentThread().getName() + "--->" + i);
            }
        }
    }
    
    class MyRunnable6 implements Runnable {
    
        @Override
        public void run() {
            for(int i = 1; i <= 10000; i++) {
                //每100个让位一次。
                if(i % 100 == 0){
                    Thread.yield(); // 当前线程暂停一下,让给主线程。
                }
                System.out.println(Thread.currentThread().getName() + "--->" + i);
            }
        }
    }
    

线程合并 join

  • 合并线程后,当前的线程会受到阻塞,直至合并过来的线程执行结束。

  • /*
    线程合并
     */
    public class ThreadTest13 {
        public static void main(String[] args) {
            System.out.println("main begin");
    
            Thread t = new Thread(new MyRunnable7());
            t.setName("t");
            t.start();
    
            //合并线程
            try {
                t.join(); // t合并到当前线程中,当前线程受阻塞,t线程执行直到结束。
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            System.out.println("main over");
        }
    }
    
    class MyRunnable7 implements Runnable {
    
        @Override
        public void run() {
            for(int i = 0; i < 10000; i++){
                System.out.println(Thread.currentThread().getName() + "--->" + i);
            }
        }
    }
    

关于多线程并发环境下数据的安全问题

  • 为什么这个是重点?

    • 以后在开发中,我们的项目都是运行在服务器当中,而服务器已经将线程的定义,线程对象的创建,线程的启动等,都已经实现完了。这些代码我们都不需要编写。
    • 最重要的是:你要知道,你编写的程序需要放到一个多线程的环境下运行,你更需要关注的是这些数据在多线程并发的环境下是否是安全的。(重点:*
  • 什么时候数据在多线程并发的环境下会存在安全问题呢?

    • 三个条件: 条件 1:多线程并发。 条件 2:有共享数据。 条件 3:共享数据有修改的行为。
    • 满足以上 3 个条件之后,就会存在线程安全问题。
  • 怎么解决线程安全问题呢?

    • 当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在线程安全问题,怎么解决这个问题?

      • 线程排队执行。(不能并发)
      • 用排队执行解决线程安全问题。
      • 这种机制被称为:线程同步机制。

      • 专业术语叫做:线程同步,实际上就是线程不能并发了,线程必须排队执行。

      • 怎么解决线程安全问题呀?

        • 使用“线程同步机制”。
    • 线程同步就是线程排队了,线程排队了就会牺牲一部分效率,没办法,数据安全第一位,只有数据安全了,我们才可以谈效率。数据不安全,没有效率的事儿。

  • 说到线程同步这块,涉及到这两个专业术语

    • 异步编程模型:
      • 线程 t1 和线程 t2,各自执行各自的,t1 不管 t2,t2 不管 t1,谁也不需要等谁,这种编程模型叫做:异步编程模型。其实就是:多线程并发(效率较高)异步就是并发。
    • 同步编程模型:
      • 线程 t1 和线程 t2,在线程 t1 执行的时候,必须等待 t2 线程执行结束,或者说在 t2 线程执行的时候,必须等待 t1 线程执行结束,两个线程之间发生了等待关系,这就是同步编程模型。效率较低。线程排队执行。同步就是排队

Java 中三大变量

  • 实例变量:在堆中。

  • 静态变量:在方法区。

  • 局部变量:在栈中。

  • 以上三大变量中:

    • 局部变量永远都不会存在线程安全问题。
    • 因为局部变量不共享。(一个线程一个栈。)
    • 局部变量在栈中。所以局部变量永远都不会共享。

  • 实例变量在堆中,堆只有 1 个。

  • 静态变量在方法区中,方法区只有 1 个。

  • 堆和方法区都是多线程共享的,所以可能存在线程安全问题。

  • 局部变量+常量:不会有线程安全问题。

  • 成员变量:可能会有线程安全问题。

  • 如果使用局部变量的话:

    • 建议使用:StringBuilder。
    • 因为局部变量不存在线程安全问题。选择 StringBuilder。
    • StringBuffer 效率比较低。
    • ArrayList 是非线程安全的。
    • Vector 是线程安全的。
    • HashMap HashSet 是非线程安全的。
    • Hashtable 是线程安全的。

Synchronized

第一种:同步代码块 //灵活

synchronized(线程共享对象){
	同步代码块;
}

第二种:在实例方法上使用synchronized 表示共享对象一定是this 并且同步代码块是整个方法体。

第三种:在静态方法上使用synchronized 表示找类锁。 类锁永远只有1把。 就算创建了100个对象,那类锁也只有一把。

对象锁:1个对象1把锁,100个对象100把锁。 类锁:100个对象,也可能只是1把类锁。

  • 银行取钱问题

    • public class Account {
          // 账号
          private String actno;
          // 余额
          private double balance;
      
          public Account() {
          }
      
          public Account(String actno, double balance) {
              this.actno = actno;
              this.balance = balance;
          }
      
          public String getActno() {
              return actno;
          }
      
          public void setActno(String actno) {
              this.actno = actno;
          }
      
          public double getBalance() {
              return balance;
          }
      
          public void setBalance(double balance) {
              this.balance = balance;
          }
      
          //取款的方法
          /*
          在实例方法上可以使用synchronized吗?可以的。
              synchronized出现在实例方法上,一定锁的是this。
              没得挑。只能是this。不能是其他的对象了。
              所以这种方式不灵活。
      
              另外还有一个缺点:synchronized出现在实例方法上,
              表示整个方法体都需要同步,可能会无故扩大同步的
              范围,导致程序的执行效率降低。所以这种方式不常用。
      
              synchronized使用在实例方法上有什么优点?
                  代码写的少了。节俭了。
      
              如果共享的对象就是this,并且需要同步的代码块是整个方法体,
              建议使用这种方式。
           */
          public synchronized void withdraw(double money){
              double before = this.getBalance(); // 10000
              double after = before - money;
              try {
                  Thread.sleep(1000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              this.setBalance(after);
          }
      }
      public class AccountThread extends Thread {
      
          // 两个线程必须共享同一个账户对象。
          private Account act;
      
          // 通过构造方法传递过来账户对象
          public AccountThread(Account act) {
              this.act = act;
          }
      
          public void run(){
              // run方法的执行表示取款操作。
              // 假设取款5000
              double money = 5000;
              // 取款
              // 多线程并发执行这个方法。
              act.withdraw(money);
      
              System.out.println(Thread.currentThread().getName() + "对"+act.getActno()+"取款"+money+"成功,余额" + act.getBalance());
          }
      }
      public class Test {
          public static void main(String[] args) {
              // 创建账户对象(只创建1个)
              Account act = new Account("act-001", 10000);
              // 创建两个线程
              Thread t1 = new AccountThread(act);
              Thread t2 = new AccountThread(act);
              // 设置name
              t1.setName("t1");
              t2.setName("t2");
              // 启动线程取款
              t1.start();
              t2.start();
          }
      }
      

Synchronized 面试题

  1. // 面试题:doOther方法执行的时候需要等待doSome方法的结束吗?
        //不需要,因为doOther()方法没有synchronized
    public class Exam01 {
        public static void main(String[] args) throws InterruptedException {
            MyClass mc = new MyClass();
    
            Thread t1 = new MyThread(mc);
            Thread t2 = new MyThread(mc);
    
            t1.setName("t1");
            t2.setName("t2");
    
            t1.start();
            Thread.sleep(1000); //这个睡眠的作用是:为了保证t1线程先执行。
            t2.start();
        }
    }
    
    class MyThread extends Thread {
        private MyClass mc;
        public MyThread(MyClass mc){
            this.mc = mc;
        }
        public void run(){
            if(Thread.currentThread().getName().equals("t1")){
                mc.doSome();
            }
            if(Thread.currentThread().getName().equals("t2")){
                mc.doOther();
            }
        }
    }
    
    class MyClass {
        public synchronized void doSome(){
            System.out.println("doSome begin");
            try {
                Thread.sleep(1000 * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("doSome over");
        }
        public void doOther(){
            System.out.println("doOther begin");
            System.out.println("doOther over");
        }
    }
    
  2. // 面试题:doOther方法执行的时候需要等待doSome方法的结束吗?
        //需要
    public class Exam01 {
        public static void main(String[] args) throws InterruptedException {
            MyClass mc = new MyClass();
    
            Thread t1 = new MyThread(mc);
            Thread t2 = new MyThread(mc);
    
            t1.setName("t1");
            t2.setName("t2");
    
            t1.start();
            Thread.sleep(1000); //这个睡眠的作用是:为了保证t1线程先执行。
            t2.start();
        }
    }
    
    class MyThread extends Thread {
        private MyClass mc;
        public MyThread(MyClass mc){
            this.mc = mc;
        }
        public void run(){
            if(Thread.currentThread().getName().equals("t1")){
                mc.doSome();
            }
            if(Thread.currentThread().getName().equals("t2")){
                mc.doOther();
            }
        }
    }
    
    class MyClass {
        public synchronized void doSome(){
            System.out.println("doSome begin");
            try {
                Thread.sleep(1000 * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("doSome over");
        }
        public synchronized void doOther(){
            System.out.println("doOther begin");
            System.out.println("doOther over");
        }
    }
    
  3. / 面试题:doOther方法执行的时候需要等待doSome方法的结束吗?
        //不需要,因为MyClass对象是两个,两把锁。
    public class Exam01 {
        public static void main(String[] args) throws InterruptedException {
            MyClass mc1 = new MyClass();
            MyClass mc2 = new MyClass();
    
            Thread t1 = new MyThread(mc1);
            Thread t2 = new MyThread(mc2);
    
            t1.setName("t1");
            t2.setName("t2");
    
            t1.start();
            Thread.sleep(1000); //这个睡眠的作用是:为了保证t1线程先执行。
            t2.start();
        }
    }
    
    class MyThread extends Thread {
        private MyClass mc;
        public MyThread(MyClass mc){
            this.mc = mc;
        }
        public void run(){
            if(Thread.currentThread().getName().equals("t1")){
                mc.doSome();
            }
            if(Thread.currentThread().getName().equals("t2")){
                mc.doOther();
            }
        }
    }
    
    class MyClass {
        public synchronized void doSome(){
            System.out.println("doSome begin");
            try {
                Thread.sleep(1000 * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("doSome over");
        }
        public synchronized void doOther(){
            System.out.println("doOther begin");
            System.out.println("doOther over");
        }
    }
    
  4. // 面试题:doOther方法执行的时候需要等待doSome方法的结束吗?
        //需要,因为静态方法是类锁,不管创建了几个对象,类锁只有1把。
    public class Exam01 {
        public static void main(String[] args) throws InterruptedException {
            MyClass mc1 = new MyClass();
            MyClass mc2 = new MyClass();
    
            Thread t1 = new MyThread(mc1);
            Thread t2 = new MyThread(mc2);
    
            t1.setName("t1");
            t2.setName("t2");
    
            t1.start();
            Thread.sleep(1000); //这个睡眠的作用是:为了保证t1线程先执行。
            t2.start();
        }
    }
    
    class MyThread extends Thread {
        private MyClass mc;
        public MyThread(MyClass mc){
            this.mc = mc;
        }
        public void run(){
            if(Thread.currentThread().getName().equals("t1")){
                mc.doSome();
            }
            if(Thread.currentThread().getName().equals("t2")){
                mc.doOther();
            }
        }
    }
    
    class MyClass {
        // synchronized出现在静态方法上是找类锁。
        public synchronized static void doSome(){
            System.out.println("doSome begin");
            try {
                Thread.sleep(1000 * 10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("doSome over");
        }
        public synchronized static void doOther(){
            System.out.println("doOther begin");
            System.out.println("doOther over");
        }
    }
    

死锁

package com.bjpowernode.java.deadlock;
/*
死锁代码要会写。
一般面试官要求你会写。
只有会写的,才会在以后的开发中注意这个事儿。
因为死锁很难调试。
 */
public class DeadLock {
    public static void main(String[] args) {
        Object o1 = new Object();
        Object o2 = new Object();

        // t1和t2两个线程共享o1,o2
        Thread t1 = new MyThread1(o1,o2);
        Thread t2 = new MyThread2(o1,o2);

        t1.start();
        t2.start();
    }
}

class MyThread1 extends Thread{
    Object o1;
    Object o2;
    public MyThread1(Object o1,Object o2){
        this.o1 = o1;
        this.o2 = o2;
    }
    public void run(){
        synchronized (o1){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o2){

            }
        }
    }
}

class MyThread2 extends Thread {
    Object o1;
    Object o2;
    public MyThread2(Object o1,Object o2){
        this.o1 = o1;
        this.o2 = o2;
    }
    public void run(){
        synchronized (o2){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (o1){

            }
        }
    }
}

开发中应该怎么解决线程安全问题

  • 是一上来就选择线程同步吗?synchronized 不是,synchronized 会让程序的执行效率降低,用户体验不好。系统的用户吞吐量降低。用户体验差。在不得已的情况下再选择线程同步机制。
  • 第一种方案:尽量使用局部变量代替“实例变量和静态变量”。
  • 第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(一个线程对应 1 个对象,100 个线程对应 100 个对象,对象不共享,就没有数据安全问题了。)
  • 第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择 synchronized 了。线程同步机制。

守护线程

java语言中线程分为两大类: 一类是:用户线程 一类是:守护线程(后台线程) 其中具有代表性的就是:垃圾回收线程(守护线程)。

守护线程的特点: 一般守护线程是一个死循环,所有的用户线程只要结束, 守护线程自动结束。

注意:主线程main方法是一个用户线程。

守护线程用在什么地方呢? 每天00:00的时候系统数据自动备份。 这个需要使用到定时器,并且我们可以将定时器设置为守护线程。 一直在那里看着,没到00:00的时候就备份一次。所有的用户线程 如果结束了,守护线程自动退出,没有必要进行数据备份了。 // 启动线程之前,将线程设置为守护线程 t.setDaemon(true);

/*
守护线程
 */
public class ThreadTest14 {
    public static void main(String[] args) {
        Thread t = new BakDataThread();
        t.setName("备份数据的线程");

        // 启动线程之前,将线程设置为守护线程
        t.setDaemon(true);

        t.start();

        // 主线程:主线程是用户线程
        for(int i = 0; i < 10; i++){
            System.out.println(Thread.currentThread().getName() + "--->" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class BakDataThread extends Thread {
    public void run(){
        int i = 0;
        // 即使是死循环,但由于该线程是守护者,当用户线程结束,守护线程自动终止。
        while(true){
            System.out.println(Thread.currentThread().getName() + "--->" + (++i));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

定时器

作用:间隔特定的时间,执行特定的程序。

每周要进行银行账户的总账操作。 每天要进行数据的备份操作。

在实际的开发中,每隔多久执行一段特定的程序,这种需求是很常见的, 那么在java中其实可以采用多种方式实现:

可以使用sleep方法,睡眠,设置睡眠时间,没到这个时间点醒来,执行 任务。这种方式是最原始的定时器。(比较low)

在java的类库中已经写好了一个定时器:java.util.Timer,可以直接拿来用。 不过,这种方式在目前的开发中也很少用,因为现在有很多高级框架都是支持 定时任务的。

在实际的开发中,目前使用较多的是Spring框架中提供的SpringTask框架, 这个框架只要进行简单的配置,就可以完成定时器的任务。

public static void main(String[] args) throws ParseException {
    SimpleDateFormat sdt=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    Date dt=sdt.parse("2020-11-25 10:14:10");
    System.out.println(dt);

    Timer timer=new Timer();
    timer.schedule(new TimerTask() {
        @Override
        public void run() {
            System.out.println("123"+dt);
        }
    }, dt, 1000 * 3);
}

实现线程的第三种方式

实现线程的第三种方式:实现Callable接口。(JDK8新特性。) 这种方式实现的线程可以获取线程的返回值。 之前讲解的那两种方式是无法获取线程返回值的,因为run方法返回void。 思考: 系统委派一个线程去执行一个任务,该线程执行完任务之后,可能 会有一个执行结果,我们怎么能拿到这个执行结果呢? 使用第三种方式:实现Callable接口方式。

/*
实现线程的第三种方式:
    实现Callable接口
    这种方式的优点:可以获取到线程的执行结果。
    这种方式的缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低。
 */
public class ThreadTest15 {
    public static void main(String[] args) throws Exception {

        // 第一步:创建一个“未来任务类”对象。
        // 参数非常重要,需要给一个Callable接口实现类对象。
        FutureTask task = new FutureTask(new Callable() {
            @Override
            public Object call() throws Exception { // call()方法就相当于run方法。只不过这个有返回值
                // 线程执行一个任务,执行之后可能会有一个执行结果
                // 模拟执行
                System.out.println("call method begin");
                Thread.sleep(1000 * 3);
                System.out.println("call method end!");
                int a = 100;
                int b = 200;
                return a + b; //自动装箱(300结果变成Integer)
            }
        });

        // 创建线程对象
        Thread t = new Thread(task);

        // 启动线程
        t.start();

        // 这里是main方法,这是在主线程中。
        // 在主线程中,怎么获取t线程的返回结果?
        // get()方法的执行会导致“当前线程阻塞”
        Object obj = task.get();
        System.out.println("线程执行结果:" + obj);

        // main方法这里的程序要想执行必须等待get()方法的结束
        // 而get()方法可能需要很久。因为get()方法是为了拿另一个线程的执行结果
        // 另一个线程执行是需要时间的。
        System.out.println("hello world!");
    }
}

Object 类中的 wait 和 notify

第一:wait和notify方法不是线程对象的方法,是java中任何一个java对象 都有的方法,因为这两个方式是Object类中自带的。 wait方法和notify方法不是通过线程对象调用, 不是这样的:t.wait(),也不是这样的:t.notify()..不对。 第二:wait()方法作用? Object o = new Object(); o.wait();

表示: 让正在o对象上活动的线程进入等待状态,无期限等待, 直到被唤醒为止。 o.wait();方法的调用,会让“当前线程(正在o对象上 活动的线程)”进入等待状态。

第三:notify()方法作用? Object o = new Object(); o.notify();

表示: 唤醒正在o对象上等待的线程。

还有一个notifyAll()方法: 这个方法是唤醒o对象上处于等待的所有线程。

wait和notify

生产消费均衡

import java.util.ArrayList;
import java.util.List;

/*
1、使用wait方法和notify方法实现“生产者和消费者模式”

2、什么是“生产者和消费者模式”?
    生产线程负责生产,消费线程负责消费。
    生产线程和消费线程要达到均衡。
    这是一种特殊的业务需求,在这种特殊的情况下需要使用wait方法和notify方法。

3、wait和notify方法不是线程对象的方法,是普通java对象都有的方法。

4、wait方法和notify方法建立在线程同步的基础之上。因为多线程要同时操作一个仓库。有线程安全问题。

5、wait方法作用:o.wait()让正在o对象上活动的线程t进入等待状态,并且释放掉t线程之前占有的o对象的锁。

6、notify方法作用:o.notify()让正在o对象上等待的线程唤醒,只是通知,不会释放o对象上之前占有的锁。

7、模拟这样一个需求:
    仓库我们采用List集合。
    List集合中假设只能存储1个元素。
    1个元素就表示仓库满了。
    如果List集合中元素个数是0,就表示仓库空了。
    保证List集合中永远都是最多存储1个元素。

    必须做到这种效果:生产1个消费1个。
 */
public class ThreadTest16 {
    public static void main(String[] args) {
        // 创建1个仓库对象,共享的。
        List list = new ArrayList();
        // 创建两个线程对象
        // 生产者线程
        Thread t1 = new Thread(new Producer(list));
        // 消费者线程
        Thread t2 = new Thread(new Consumer(list));

        t1.setName("生产者线程");
        t2.setName("消费者线程");

        t1.start();
        t2.start();
    }
}

// 生产线程
class Producer implements Runnable {
    // 仓库
    private List list;

    public Producer(List list) {
        this.list = list;
    }
    @Override
    public void run() {
        // 一直生产(使用死循环来模拟一直生产)
        while(true){
            // 给仓库对象list加锁。
            synchronized (list){
                if(list.size() > 0){ // 大于0,说明仓库中已经有1个元素了。
                    try {
                        // 当前线程进入等待状态,并且释放Producer之前占有的list集合的锁。
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 程序能够执行到这里说明仓库是空的,可以生产
                Object obj = new Object();
                list.add(obj);
                System.out.println(Thread.currentThread().getName() + "--->" + obj);
                // 唤醒消费者进行消费
                list.notifyAll();
            }
        }
    }
}

// 消费线程
class Consumer implements Runnable {
    // 仓库
    private List list;

    public Consumer(List list) {
        this.list = list;
    }

    @Override
    public void run() {
        // 一直消费
        while(true){
            synchronized (list) {
                if(list.size() == 0){
                    try {
                        // 仓库已经空了。
                        // 消费者线程等待,释放掉list集合的锁
                        list.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 程序能够执行到此处说明仓库中有数据,进行消费。
                Object obj = list.remove(0);
                System.out.println(Thread.currentThread().getName() + "--->" + obj);
                // 唤醒生产者生产。
                list.notifyAll();
            }
        }
    }
}

常见报错

  • 空指针异常:NullPointerException
  • 类型转换异常:ClassCastException
  • 数组下标越界异常:ArrayIndexOutOfBoundsException
  • 数字格式化异常:NumberFormatException