20221011 review java

2022/10/11 review java

1. 面向对象(上)

1.1学习Java的三条主路线:

1) Java类及其成员:属性,方法,构造器,内部类,代码块。

2)面向对象的三大特征:封装性,继承性,多态性。

3)其他关键字:this,super,static,final,abstract,interface,package,import。

1.2面向对象的理解:

1)对象的行为封装到类中,每一种行为都是一种方法。

2)所以面向对象强调具备了功能的对象,考虑谁来做什么。

1.3类的理解:

1)属性和功能组成的整体可以叫做类

2)类一般是现实中的带有不同修饰的名词

3)如果该名词只有单一作用,如钞票1000,则是属性

4)类是抽象的,对象为实例。

5) 面向对象程序设计的重点是对类的设计

6)设计类,就是设计类的成员和功能。

1.4类和对象的使用

1)创建类,设计类的成员

2)创建类的对象

3)用“对象.属性or方法”调用对象的结构

1.5类的多个对象的赋值关系

  • p1和p2同时指向同一个堆空间,如果修改其一对象值,都会导致堆空间变化。
public class SameAddresses {
    public static void main(String[] args) {

        People p1 = new People();

        //给p1赋值
        p1.name = "郭大侠";
        p1.age = "20";
        p1.isMale = true;

        //将p1数据赋值给p2
        People p2 = p1;
        //输出p2对象的值
        System.out.println(p2.name+p2.age+p2.isMale);//此时p2指向p1的堆空间
        
        //p1和p2同时指向同一个堆空间,如果修改其一对象值,都会导致堆空间变化
        p2.age = "10";             
        System.out.println(p1.age);
        System.out.println(p2.age);
    }
}
//设计类的属性和功能
public class People {
    String name;
    String age;
    Boolean isMale;
}

1.6属性与局部变量的关系

1)不同点:

​ **1. **在类中声明的位置不同,

​ 属性在类{}中,

​ 局部变量在方法内,方法形参,代码块内,构造器形参,构造器内部的变量。

​ **2. **使用权限修饰符的不同

​ 属性:private ,public ,protected ,默认

​ **3. **默认初始化值的不同

​ 属性:都有其默认初始化值

​ 引用数据类型(类,数据,接口), null

​ 布尔值(boolean), false ........................

1.7 使用方法的注意事项:

  • 方法中不能定义方法
  • 有返回值的类型的方法必须有return 值
  • 返回值的类型必须与方法要返回的类型一致

1.8 类的设计

1)分开工具类和测试类

image-20221012090223300

2)抽象的重复的动作放在工具类中,可以反复使用

public class PeopleUtil {

    public String joint(String name){
        System.out.println(name+"睡好吃好");
        return name;
    }
}

3)main方法中写测试

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

        PeopleUtil p = new PeopleUtil();
        System.out.println("请输入一个名字");
        //调用方法
        p.joint("郭大侠");
    }
}

1.9 Jvm的规范

  • 虚拟机栈,就是平时提到的栈,将局部变量存储在栈结构中
  • 堆,new出来的结构比如数组,对象,加在堆空间中
  • 对象的属性(非static)也加载在堆空间中

方法区:类的加载信息 、常量池、静态域

1.10 引用型参数的内存解析

1) 存储两类值:要么是null,要么是地址值

1.11匿名对象的使用(开发常用)

 public static void main(String[] args) {
    people p = new people();
     
                //匿名对象
                p.show(new Phone);
 }

class Phone{
    //方法
    public void show(Phone phone){
        phone.eat();
        phone.sleep();
    }
}

**代码解析: **

  • 实参(new Phone)传递给形参时,而形参位置是局部变量,存于栈中,即phone变量放在栈中,可以用使用多次了,我们拿phone地址所代表的对象调用方法。

1.12数组的基本运算操作

  • 求Max
public int getMax(int[] arr){

    //具体操作求Max
    int maxNumber = arr[0];
    for (int i =1;i< arr.length;i++){
        if (maxNumber<arr[i]){
            maxNumber =arr[i];
        }
    }
    return maxNumber;
}
  • 求Min
public int getMin(int[] arr){

    //具体操作求Min
    int minNumber = arr[0];
    for (int i =1;i<arr.length;i++){
        if (minNumber>arr[i]){
            minNumber =arr[i];
        }
    }
    return minNumber;
}
  • 求Sum
public int getSum(int[] arr){

    //具体操作求总和
    int sum = 0;
    for (int i =0;i<arr.length;i++){
        sum +=arr[i];
    }
    return sum;
}
  • 求复制
public void cope(int[] arr){

    //具体操作求复制
    int[] arr1 = new int[arr.length];
    for (int i = 0;i< arr.length;i++){
        arr1[i] = arr[i];
        System.out.print(arr1[i]);
    }
    System.out.println();
}
  • 求反转
public void reverse(int[] arr){

    //具体操作求反转
    for (int i = 0;i< arr.length/2;i++){
        int temp = arr[i];
        arr[i] = arr[arr.length-1 -i];
        arr[arr.length-1 -i] = temp;
    }
    //遍历输出
    for (int j = 0;j<arr.length;j++){
        System.out.print(arr[j]);
    }
    System.out.println();
}
  • 求指定元素的索引或数组下标
public int getIndex(int[] arr,int dest){

    //具体操作求指定元素
    for (int i = 0;i< arr.length;i++){
        if (dest ==arr[i]){
            return i;
        }
    }
    return  -2;
}
  • 求数组排序(使用冒泡排序)
public void sort(int[] arr){

    //冒泡排序

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

        System.out.print(arr[i]);
    }
}

1.13 方法的重载

  • 概念:在同一个类中,允许存在同名方法,只要这些方法的参数个数或者参数类型不同就好
  • 特点:与返回类型无关;只看参数列表

1.14可变个数形参的方法(新特性)

  1. 可变形参的具体细节:
  • 格式: 数据类型 ...变量名
  • 传入的参数个数:任意个
  • 同名方法同个数形参的数组的方法与可变形参的方法不能同时存在

image-20221012182352106

  • 如果一个方法中存在多个形参变量,则可变形参放在最后一个位置
  • 最多在方法中声明一个可变形参

1.15 值传递机制

  • 引用数据类型:如果形参为引用类型,则等于new 了一个对象,在堆空间开辟位置存放实参的值

  • 基本数据类型:形参变化改变不了实参,因为形参放在栈空间,用完即销毁

  • 基本数据类型:传递的是数据值 int a=1;int b=a;

  • 引用数据集类型:传递的是地址值,其中包括变量的数据类型 People p=new People(); User u≠p;

  • java中方法形参的传递机制是声明?值传递。

1.16 递归方法的使用

  • 递归1

1+2+3+4+5.....+i

public int getSum(int i){

    //递归求 1+2+3+4+5.....+i=?

    if (i==1){
        return 1;
    }else {
        return i +getSum(i-1);
    }
}
  • 递归2

​ f(1)=1,f(2)=4,f(n+2)=2*f(n+1)f(n)

public int f(int n){
    if (n==0){
        return 1;
    }else if (n==1){
        return 4;
    }else {
        return 2*f(n-1)+f(n-2);
    }
}
  • 递归3

斐波那契数列 (Fibonacci) 1 ,1 ,2 ,3, 5, 8, 13, 21 ,34, 55

一个数等于前两个数之和 F (0)=0,F (1)=1, F (n)=F (n-1)+F (n-2)(n>=2,n∈N*)

public int fibonacci(int n){
    if (n==0){
        return 0;
    }else if (n==1){
        return 1;
    } else if (n==2) {
        return 1;
    }else{
        return fibonacci(n-2)+fibonacci(n-1);
    }
}
  • 快排

1.17封装性

  • 由来:

当我们创建一个类的对象后,我们可以通过“对象.属性”的方式,对对象的属性进行赋值。这里,赋值操作要受属性的数据类型和存储范围的制约。除此之外,没有其他的制约条件。但是,在实际问题中,我们往往需要给属性赋值时加入一些额外的限制条件。这个条件就不能在属性声明时体现,我们只能通过方法进行限制条件的添加。(比如:避免有setXXX()时,再对“对象.属性”调用时对属性赋值,则需要将属性声明为私有的(private)

这一思想体现了封装性思想。

  • 封装性的体现:
      1. 类的属性私有化(private),提供公有的(public)方法获取(getXXX)和设置(setXXX)
      2. 私有方法,仅供本类使用
      3. 单例模式....................

1.18 构造器

  • 作用:
  1. 创建对象 People p = new Peopel();

  2. 初始化对象的信息 People p = new Peopel(Tom);

  • 说明使用:
  1. 如果没有显示定义类的构造器的话,则默认提供一个带空参的构造器,开发中,如果需要有参构造器,则习惯提供一个空参构造器
  2. 定义一个构造器的方法:权限修饰符 类名(形参列表)
  3. 如果类中定义多个构造器,则构成重载。
  4. 一旦我们使用带参构造器,则需要我们自己提供一个空参的构造器。
  5. 一个类中,至少有一个构造器
public class TriAngle {

   private double base; //三角形底边
    private double height; //三角形的高

    //构造器
    public TriAngle(){}        //提供空参构造器
    public TriAngle(double base,double height){
        this.base = base;
        this.height = height;
    }

    //通过setXX()方法设置封装起来的属性
    public void setBase(double base){
        this.base = base;
    }
    public void setHeight(double height){
        this.height = height;
    }
    //通过getXX()方法使用属性
    public double getBase(){
        return base;
    }
    public double getHeight(){
        return height;
    }
}
public class TriAngleTest {
    public static void main(String[] args) {
        TriAngle ta = new TriAngle();
        ta.setBase(2.0);//设置低为2
        ta.setHeight(2.0);//设置高为2
        System.out.println(ta.getBase()+ta.getHeight());
    }
}

MVC设计模式

image-20221013204311313

个人理解:三层分别为视图层、服务层、数据层。

2. 面向对象(中)

2.1 继承性

  • 格式:class A extend B{}
  • 体现,一旦子类A继承父类B后,子类A中就获得了父类中声明的所有的属性和方法;
  • 特别的,父类中有private权限声明的属性或方法,子类继承父类后,仍然认为获取了父类中的私有结构;
  • 只是因为封装性的影响,使得子类不能直接调用父类的结构而已,换句话说,子类没有权限使用父类声明为private的属性或方法而已。

2.2 重写方法

面试题:重载与重写的区别:

  1. 重载:

    • 方法名必须一致
    • 方法修饰权限和返回值类型不受限制
    • 只要形参位置的类型不一样,就构成重载:
      • 可能情况1:两个方法中的形参一样,但位置不同;
      • 可能情况2:形参个数不同;
  2. 重写:

    • 重写父类同名同参方法,修改父类的内容
    • 所以重写方法必须与父类同名同
  • 概念:子类继承父类后,可以对跟父类同名同参的方法进行覆盖。(好比煎饼)
  • 方法的声明: 权限修饰符 返回值类型 方法名(形参列表){}
  • 规定: ①父类中同参同名方法; ②子类中同参同名方法
  1. ①的权限修饰符为private,②不能有重写
  2. ②的权限修饰符必须大于等于①,即子类的权限更大
  3. ①的返回值类型为void,②也必须只能为void
  4. ①的返回值类型为基本数据类型,②也必须为基本数据类型
  5. ①的返回值类型为A类型,②可以是A类型,也可以是A类型的子类
  6. ①②要么都声明为非static(可以重写),要么都不能声明为static(不是重写)

2.3重写toString的理解

java中默认的toString方法来自Object 类,在Java中每个类都直接或者间接继承Object 类,toString() 方法同样来自于Object 类。在没有重写tostring的前提下,每次执行System.out.println() 这个方法默认就会调用一个继承自Object 类型对象的toString 方法,打印出字符串,如果我们打印的是一个类对象,打印出来的是一串地址值getClass().getName()+’@’+Integer.toHexString(hashCode()),不符合人性化,所以才需要重写Object类中的toString方法输出我们想要的内容

2.4this和super关键字

this:

  1. 调用当前方法的属性或方法
  2. this关键字从子类中的属性或方法查找
  3. 子类的空参构造器默认调用super();访问父类的空参构造器

super:

  1. 调用父类方法的属性或方法
  2. super关键字从父类中查找

相同点:

  1. 在构造器里面,this(形参列表);super(形参列表);只能放在首行
  2. this(形参列表);super(形参列表);只能出现一个,才能满足1

2.5子类对象的实例化

  • 当我们用子类的空参构造器创建对象时,系统默认使用super()直接或间接调用父类的构造器,进而形成一层又一层子类调用父类构造器,直到我们调用包java.lang.Object中的空参构造器。

2.6多态性

使用前提:

  1. 类的继承关系
  2. 方法的重写

多态理解:

  1. 父类的引用指向子类的对象
  2. 编译,看左边;运行,看右边。

例如:People p1 = new boy();

  • 编译时看父类中方法,运行时看子类中重写方法

2.7向下转型

编译时看父类方法,所以对象实例化后,如果子类中的方法除了与重写父类方法外,还有子类特有的方法时,那么该实例化对象是无法调用子类特有的方法的;但是,内存上实际是有加载进子类特有的属性和方法的,只是因为该实例化对象声明为父类类型People;解决办法就是对实例化这一过程进行向下转型。

如何向下转型呢?在基本数据类型中,如果我们想将高级数据类型double转为低级int,需要使用强制类型转换符。double d; int i =(int) d;

在基本数据类型中,强转型会导致精度损失;在多态实例化过程中,也可能出现异常。

引入关键字instanceof

  1. 使用:a instanceof A :如果对象a是类A的实例,返回true;否则false。
  2. 在向下转型前,用于判断要不要进行转型。

2.8 ==与equals的区别

== 运算符

  1. 可以使用在基本数据类型和引用数据类型中

  2. 如果比较的是基本数据类型变量,比较的是两个变量所保存的数据是否相等,这里不一定要数据类型一样

  3. 如果比较的是引用数据类型变量,比较两个变量的地址是否相等,即两个引用变量是否指向同一个对象实体

    特别的

注意:==运算符使用时,必须保证符号两边的变量类型一致;

equas() 方法

  1. 只适用于引用数据类型
  2. Object类中equals()的定义: public boolean equals(Object obj){ return (this==obj);} 说明:Object类中的equals()方法与==运算符作用相同,都是比较的是两个对象的地址值是否相等
  3. 而String、 Date、 File、包装类都重写了Object类中的equals()方法。重写以后,比较的是两个对象“实体内容”是否相等。
  • String引用数据类型中equals()部分源码
public boolean equals(Object obj) {
    if (this == obj) {
        return true;
    }
    if(obj instanceof 当前类名){
        r
    }
    //........
}

面试题:往往会问用equals()中比较两个变量的数据是否相等,我们会刻板印象的误以为是相等的,这是由于平时接触的都是String包下的equals(),里面是已经重写好equals()

2.9包装类

  • 基本数据类型——>>包装类 : 调用包装类的构造器

  • 反之:调用包装类的xxxValue()得到基本数据类型数据

  • 基本数据类型、包装类——>>String类型:调用String类里的ValueOf(XXX xxx)

  • 反之:调用包装类中的parsexxx()

3. 面向对象(下)

3.1 static关键字

  • static可以修饰:属性,方法,代码块,内部类

属性:

  1. 按是否使用static修饰,分为静态属性(类变量)vs非静态属性(实例变量)

实例变量:创建多个对象时,多个对象设置同一个属性时,不会导致另一个对象的属性修改

静态变量:创建多个对象是,多个对象共享同一个静态变量,当通过某一对象修改静态变量值时,会导致其他对象调用该静态变量时,是已经修改后的值了。

  1. 静态变量随着类的加载而加载,可以通过类.静态变量的方式调用;
  2. 静态变量的加载要早于实例变量的创建;
  3. 由于类只会加载一次,所以静态变量在内存中也只有一份,存在于方法的静态域

方法:

  1. 静态方法vs普通方法
  2. 静态方法,只能调用静态方法或属性;普通方法,都可以调用任何方法和属性,但开发中没必要用static修饰的静态方法和属性。
  3. 静态方法和属性的使用,一般是从类的生命周期去理解的。随着类的加载,静态方法和属性存在,静态方法早于普通方法,如果要使用到静态方法,一般是用类.静态方法方式使用。
  4. 开发中,什么情况下需要使用到static关键字?

操作静态属性的方法,如getXxx(),setXxx()

工具类中的方法,如Math、Arrays、Collections。

3.2 单例设计模式

  • 所谓类的单例模型,在设计工程中,对某个类只存在一个实例化对象
public class SingletonTest1 {
    public static void main(String[] args) {
        Singleton s1=Singleton.getSingleton();
        Singleton s2=Singleton.getSingleton();
        System.out.println(s1==s2); //判断两个对象是否地址值相同,结果为true
    }
}
class Singleton{
    //1.私有化构造器
    private Singleton(){}
    //2.创建私有静态的对象属性
    private static final Singleton singleton = new Singleton();
    //3.提供静态方法getXxx()返回对象属性
    public static Singleton getSingleton(){
        return  singleton;
    }
}

一:单例设计模式饿汉式实现

1.私有化构造器

2.类中创建静态的对象属性

3.提供静态方法getXxx()返回对象属性

好处: 外部类无法实例化对象,也就是无法创建多个对象

public class SingletonTest2 {
    public static void main(String[] args) {
        Singleton1 s1 =Singleton1.getInstance();
        Singleton1 s2 =Singleton1.getInstance();
        System.out.println(s1==s2);
    }

static class Singleton1{
   //1.私有化构造器
   private  Singleton1(){}
   //2.声明私有静态对象属性
   private static Singleton1 instance =null;
   //3.提供静态getXxx()方法返回对象属性
   public static Singleton1 getInstance(){
       if (instance==null){
           instance = new Singleton1();
       }
           return instance;
     }
   }

二:单例设计模式懒汉式实现

  1. 私有化构造器
  2. 声明私有静态对象属性
  3. 提供静态getXxx()方法返回对象属性

饿汉式模式vs懒汉式模式

饿汉式:好处:线程安全; 坏处:对象的加载时间拉长

懒汉式:好处:延长对象创建; 坏处:存在线程不安全..........todo

3.3 抽象类abstract

  1. abstract修饰的类:抽象类
  • 该类不能实例化创建对象
  • 抽象类中一定有构造器,便于子类实例化调用
  • 开发中,都会提供抽象类的子类,让子类实例化,完成相关的操作
  1. abstract修饰的方法:抽象方法
  • 抽象方法只有声明,没有方法体
  • 子类重写父类中所有抽象方法后,子类才可以实例化
  • 如果子类没有重写父类所有抽象方法还要实例化,就要用abstract修饰子类

3.4 匿名类

在调用方法时,需要传递一个类的对象作为实参,可以使用到匿名形式

格式: method(new 类名(){ 重写方法})

3.5 模板方法的设计模式

将不确定的部分,写成抽象方法,暴露外面,让子类重写的方法具体做;将确定部分,写于父类中。

这就是抽象类的应用。

abstract class Template{

    public void spendTime(){

        long start = System.currentTimeMillis();

        this.code();     

        long end = System.currentTimeMillis();

        System.out.println("计算1000以内质数所花费的时间:"+(end-start));
    }
    public abstract void code(); //抽象出来,让子类具体做想做的事
}
for (int i =2;i<1000;i++){
        boolean isFlag =true;

        for (int j = 2;j<=Math.sqrt(i);j++){//过滤掉不是质数
            if (i%j==0){ //如果可以除尽,说明不是质数,终结循环
                isFlag=false;
                break;
            }
        }
        if (isFlag){
            System.out.println(i);
        }
}

3.6 接口interface

  1. 接口的理解

Java开发中,接口通过让类去实现(implements)的方法来使用

如果实现类覆盖了接口中的所有抽象方法,则该实现类可以实例化对象

如果实现类没有覆盖接口全部抽象方法,则该类为抽象类。

子类是我,接口是师傅,父类是父亲,师傅传武功我,父亲传基因于我。

  1. 接口的使用:
  1. 满足多态性
  2. 是一种规范
  3. 面向接口编程
  1. 接口的匿名实现类的匿名对象:
graph TD; Flash类-->USB类; Printer-->USB类; USB类-->Computer类;

USB类为接口,Flash类和Printer为接口的实现类

在main主方法中,对Computer类实例化对象c,对象c调用Computer类中的成员方法,实参匿名化

格式c.成员方法(new USB(){重写接口所有抽象方法}))

"new USB(){重写接口所有抽象方法})"就是接口的匿名实现类的匿名对象

3.7 接口应用:代理模式

两个接口实现类通过接口可以把实现类分为代理类和被代理类

3.8接口应用:工厂模式

创建对象给工厂做,在main方法中分离创建对象和调用对象

抽象类和接口的区别:

  • 都无法实例化
  • 都是抽象的
  • 类与类,单继承;接口与接口多继承;接口与类,多实现
  • 再说明一下抽象类的定义和接口的定义

4.异常处理

个人理解:当我们打游戏时,网络掉线,提示:没联网,游戏退出。这就是对异常进行处理,异常部分直接运行时直接略过,保证整个游戏因掉线不会奔溃。

  1. try{}catch(){}finally{}:真正处理掉异常的包起来的代码,finally可有可无,若有,则说明最后一部分无论前面是否有异常,我都会执行finally里面的代码。
  2. throws+异常类型:如果有异常,则抛出异常,给其他类处理,异常代码后的所有代码都不会执行到。
  3. 开发中,如何选择异常处理:
  • 情景一:如果父类中出现异常了,没有用throws抛出,则子类也不能用throws抛出,必须用try{}catch(){}。
  • 情景二:方法中嵌套了方法,在main主方法中调用最外层方法,用try{}catch{}处理异常;有递进关系的方法都抛出异常,即用throws+异常类型。这样可以简洁代码。
  1. 手动抛出异常

格式:throw new 异常类型。异常类型常用:RuntimeException(message)Exception(message)

在可能出现异常的地方手动添加throw ,生产一个异常类型;我们引入异常类型,还要在类中用throws+异常类型或者try—catch的方式抓异常

项目2:《开发团队调度软件》

涉及知识:

问题:

  1. 为什么在Status类中要重写toString?

难点:

  1. 角色的不同需要使用不同的设备,封装成方法,当角色分成三类时,分别使用到方法的形参。
  2. 初始化数组,索引位置的选择。

5.多线程

有趣知识

以前cpu是单核的,目前有c盘,d盘,e盘,c盘有一份数据。问:同时将c盘数据复制到d盘一份,e盘一份,还是先将c盘数据复制到d盘好了后再c盘数据复制到e盘,这两种方式哪种传输数据更快些?

1. jdk5.0之前两种创建多线程

一:第一种创建多线程方法Thread

  1. 创建Thread的子类
  2. 重写Thread类的run()方法
  3. main方法,创建子类对象并对象.start()

start方法作用:

  1. 启动线程当前对象的线程
  2. 调用当前线程的run方法

如何创建多个线程:创建多个对象,并start开启线程。

或者使用匿名内部类形式

main{
    new Thread(){
    @Override
    public void run(){  //具体要执行的代码} }.start();
    }

二:第二种创建多线程方式实现Runnable接口

  1. 创建一个实现Runnable接口的类
  2. 实现类重写Runnable接口的run方法
  3. 创建实现类对象
  4. 把实现类对象作为Thread构造器参数
  5. 调用start方法

2. 多线程中常见方法使用

Thread中的常见方法 主要功能
start() 启动线程;调用run()
run() 线程要执行的内容放此方法体内
yield() 释放当前线程
currentThread() 静态方法,返回当前线程名称 “Thread-1:”
setName() 设置线程名称
getThread() 获取当前线程名字
join() 主动插队抢占cpu资源直到当前线程终结死亡
sleep(long milliTime) 指定当前线程睡眠多少毫秒。在指定时间内当前线程是阻塞状态
isAlive() 是否还存活着

修改主线程名字的方式:

  1. currentThread.getName("");
  2. 在Thread的子类中提供带参构造器,参数类型:String

修改其他线程名字的方法:

  1. currentThread.getName("");
  2. 对象.getName("");
  3. 在Thread的子类中提供带参构造器,参数类型:String

3. 线程优先级的设置

  • setPriority(int p) getPriority()

  • 参数p的取值:Max_PRIORITY:10 MIN_PRIORITY:1 NORM_PRIORITY:5

  • 高优先级的线程要抢占有低优先级线程cpu的执行权,但是只是从高概率上讲,并不意味着高线程执行完后低线程才执行

4. 多线程的卖票问题

问题描述:有三个窗口,一个卖票员手里有100张票,游客买票。

初步理解:三个窗口代表着三线程,游客们分别在三窗口排队买票,因为总共才100张票,所以设置变量为100,并且因为三个窗口是共享100张票的,所以应该共享。

代码理解:实例化三个线程时,Java虚拟机的堆会同时创建三份 同名属性。但,我们只有一份100张票,故三个对象只能共享,所以使用static关键字声明属性。

5. 同步线程安全问题

方式一:同步代码块

  1. 格式:synchronized(同步监听器){ //需要被同步的代码。}
  2. 说明:需要共享数据的代码即为需要被同步的代码;共享数据就是多个线程对象共同操作的属性变量;同步监听器即为锁;任何对象都可充当一把锁
  3. 情况:多个线程独享需用一把锁操作共享数据。

如何获得锁?

方式一:this ,当前对象

方式二:当前类.class() ,类加载只有一次

方式三:new一个任意对象

方式二:同步方法

  1. 使用:用关键字声明synchronized

继承Thread类的多线程:因为实例化后,Thread子类随着加载,子类对象同时创建三个线程调用属性变量,在堆空间会有三分属性变量,我们只想要对象使用同一个属性变量,因此我们可以将属性声明为static类型的。

实现Runnable接口的多线程:将需要同步的代码封装成方法

  1. 说明:
  • 如果同步方法为静态,那么当前类的锁是:当前类.class
  • 如果同步方法为非静态,那么当前锁是: this

6. 线程安全的单例模式

懒汉式
class Bank {

    private Bank() {}

    private static Bank instance = null;

    public static synchronized Bank getInstance() {
        if (instance == null) {
            synchronized (Bank.class) {
                if (instance == null) {
                    instance = new Bank();
                }
            }
        }            
            return instance;
    }
}
       

7. Lock同步锁

synchronized与Lock的异同:

  • 相同:都解决了线程安全问题
  • 不同:synchronized机制在执行完代码后,自动解锁;Lock需要手动启动锁和解锁
  • 建议用后者。
public class LockTest {
    public static void main(String[] args) {
        Lock l = new Lock();

        Thread t1 = new Thread(l);
        Thread t2 = new Thread(l);

        t1.setName("窗口一");
        t2.setName("窗口二");

        t1.start();
        t2.start();
    }
}
class Lock implements Runnable{

    private int ticket = 100;

    //1. 实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true){
            try {
                //2. 调用锁定方法lock()
                lock.lock();
                if (ticket>0){
                    try {
                        Thread.sleep(100);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
            System.out.println(Thread.currentThread().getName()+":售票,票"+ticket);
            ticket--;
                }else{
                    break;
                }
            }finally {
                //3.解锁方法unlock()
                lock.unlock();
            }

        }

    }
}

8. 生产者与消费者单例模式

  • 背景:生产者生产产品,消费者消费产品,店长从生产者拿到产品放在前台售卖给消费者
  • 冲突:因为柜台空间有限,所以假设一次只能放20个产品供销售。
  • 问题:如何合理控制产品的数量?
  • 解决:如果柜台没有产品,店长通知生产者生产产品,并让消费者等待;如果柜台产品超过20,店长让生产 者等待。
  • 分析:
    1. 是否为多线程问题? 是,有生产者线程和消费者线程
    2. 是否有共享数据? 是,店长和产品数量
    3. 怎么解决线程安全问题? 同步机制,有三种方式
    4. 是否涉及线程的通信? 是,需要使用wait()方法等待
  • 步骤:
  • 使用继承Thread多线程
    1. 创建店长类
    2. 创建生产者类,将店长类作为属性变量,并提供有参构造器,参数为店长类对象
    3. 创建消费者类,将店长类作为属性变量,并提供有参构造器,参数为店长类对象
    4. 生产者类继承Thread,并重写run方法
    5. 消费者类继承Thread,并重写run方法
    6. 在店长类中,声明产品数量,一开始为0个;定义生产产品和消费产品的方法
    7. 在生产方法中,如果产品大于20,不再生产,让生产者等待
    8. 在消费方法中,如果没有产品,让消费者等待
    9. run方法调用店长类中的方法
    10. 在mian方法启动线程
 class Clerk {

    private  static int productNum =0;
    //生产产品
    public  synchronized void produceProduct() {
        if (productNum<20){
            productNum++;
            System.out.println(Thread.currentThread().getName()+"正在生产产品"+productNum);
           
            notify();
        }else {
            try {
                wait();
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
        }
    }
    //消费产品
    public synchronized void cusumeProduct() {
         if (productNum>0){
            System.out.println(Thread.currentThread().getName()+"正在消费产品"+productNum);
            productNum--;
            
            notify();
        }else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Producter extends Thread {

    private  Clerk c;

    public Producter( Clerk c) {
        this.c = c;
    }
    @Override
    public void run() {
        //具体生产用一个方法
        System.out.println(getName()+":开始生产产品.....");
        while (true){
            try {
                sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            c.produceProduct();

        }

    }
}
public class Custormer extends Thread {
    private Clerk c ;
    public Custormer(Clerk c) {
        this.c = c;
    }

    @Override
        public void run() {
            //具体生产用一个方法
            System.out.println(getName()+":开始消费产品.....");
        while (true) {
            try {
                sleep(11);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
             c.cusumeProduct();

        }

        }
    }
public class ProductorTest {
    public static void main(String[] args) {
           Clerk c = new Clerk();

           Producter p1= new Producter(c);
           Custormer c1 =new Custormer(c);

           p1.setName("生产者1");
           c1.setName("消费者1");

        p1.start();
        c1.start();
    }
}

总结:

  • 线程安全疑点:在哪里唤醒线程,生产完一个产品后,就要唤醒消费者线程,在生产后加上wait();消费完一个产品后,就要唤醒生产者者线程,在消费后加上wait()。

wait():唤醒的是:除了当前线程外的其他线程中的任意一个线程

wait():唤醒的是:除了当前线程外的其他全部线程

9. jdk5.0之后的两种创建多线程方式

三:实现Callable接口的方式

  1. 创建一个实现Callable接口的实现类
  2. 实现call方法,把线程需要操作声明在call()方法中
  3. 创建Callable接口的实现类对象
  4. 创建FutureTask类对象,并将Callable接口的实现类对象作为参数传递给FutureTask类
  5. 将FutureTask类对象作为参数传递给Thread类,Thread调用start()启动线程
  6. 用FutureTask特有方法get(),获取call()方法中的返回值

get()返回值:是FutureTask构造器参数Callable实现类重写的call()方法中的返回值。

使用实现Callable接口的优势:

  • 有返回值,可以提供给其他线程使用
  • 可以抛异常,方便其他线程知道该线程有异常要处理
  • 使用了泛型.........

四:使用线程池的方式

背景:经常创建和销毁,使用量特大的资源,比如并发情况下的线程,对性能影响很大。

思路:提前创建多个线程,放在线程池中,使用时直接获取,使用完后放回线程池

好处:提高响应速度;减少资源消耗;便于线程管理。

corePoolSize:核心池的大小

maximumPoolSize:最大线程数

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

步骤:

  1. 创建池 ExecutorService service = Executors.newFixedThreadPool(n);
  2. 调用方法,参数为Runable接口实现类或Callable接口实习类 execute()、submit()
  3. 关闭池 shutdown()

问题:在其他方式创建多线程中,都可以设置线程属性,这里ExecutorService是接口,无法设置,因为接口抽象的,一般交给接口实现类完成具体操作,那么如何获取ExecutorService接口的实现类呢?

解决:向下转型,这里把接口转为实现类ThreadToolExecutor,再调用实现类ThreadToolExecutor中的setCorePoolSize() setkeepAliveTime().

6. 枚举类

  • 自定义

1.0使用enum关键字

步骤:

  1. enum代替class声明类
  2. 常量名(对象属性实参)
  3. 声明对象属性:private final 修饰
  4. 私有化构造器,提供带参
  5. 提供其他诉求:提供getXXxx方法获取枚举类属性

enum枚举类的父类是java.lang.Enum,父类重写toString的输出格式:常量名

题:定义时间常量:分,小时,天的枚举类

//1. enum代替class声明类
enum time{
    //2. 常量名(对象属性实参)
    MIN(60),
    HOUR(24),
    DAY(30);
    //3. 声明对象属性:private final 修饰
    private final int NUM;
    //4. 私有化构造器,提供带参
    private time(int NUM){
        this.NUM = NUM;
    }
   //5. 提供其他诉求:提供getXXxx方法获取枚举类属性
    public int getNUM() {
        return NUM;
    }
}

2.0使用enum关键字定义的枚举类实现接口

  1. 实现接口
  2. 在常量名重写接口方法
image-20221022230406103

image-20221022231238082

7.注解 (Annotation)

1.0概述

  1. jdk5.0新增特性
  2. 其实就是代码里的特殊标记,这些标记可以在编译,类加载,运行时被读取,并保证程序员可以不改变原有逻辑的情况下,在源文件中嵌入一些补充信息。
  3. 在JavaEE中,注解占据了重要角色,例如用来配置应用程序的任何切面,代替JavaEE旧版本所遗留的繁冗代码和XML配置等。
  4. JDK内置的注解:@Overrde @Deprecated @SuppressWarnings

2.0元注解

如何自定义注解:参照JDK内置注解定义

  1. 注解声明为:@interface
  2. 部定义成员,通常使用value表示
  3. 可以指定成员的默认值,使用default定义
  4. 如果自定义注解没有成员,表明是一个标识作用

注意点:

  1. 如果注解有成员,在使用注解时,需要指明成员的值。
  2. 自定义注解必须配上注解的信息处理流程(如反射)才有意义
  3. 之定义注解通常都会指明两个元注解:Retention,Target
  4. jdk提供的4种元注解:元注解就是对现有的注解进行解释说明的注解

Retention:指定所修饰的注解的生命周期:SOURCE\CLASS\RUNTIME

Target:用于指定被修饰的注释能用于修饰哪些程序元素

Documented:所修饰的注解会在javadoc解析时被保存下来

Inherited:继承性

  1. 通过反射机制获取注解信息.....................

3.0 jdk 8 注解新特性

  1. 可重复注解
  • 在MyAnnotation上声明@Repeatable,成员值为MyAnnotation.class.
  • MyAnnotation的Target与Retention与Inherited和MyAnnotation相同。
  1. 类型注解
  • ElementType.TYPE_PARAMETER 表示该注解能写在类型变量的声明语句中(如:泛型声明)。
  • ElementType.TYPE_USE 表明该注解能写在使用类型的任何语句中。

8.集合

1.0概述

  1. 集合和数组都是对多个数据进行存储操作的结构,简称Java容器。

存储:主要指的是内存层面的存储,不涉及到持久化的存储(.txt,jpg,avi,数据库)

  1. 数组在存储多个数据方面的特点:
  • 一旦初始化后,长度确定,类型也确定

String[] arr; int[] arr; Object[] arr;

  1. 数组缺点:
  • 长度不可修改
  • 数组提供的方法有限,对于增删改等操作很不方便,效率也不高
  • 在获取数组中实际元素的个数的需求上,没有相应的属性和方法可用
  • 数组存储数据的特点:有序,可重复,对于无序和不重复的需求,不能满足

2.0框架

  1. Collection接口:单例集合,用来存储一个一个的对象
  • List:存储有序,可重复的数据 -->"动态"数组

ArrayList、LinkedList、Vector

  • Set:存储无序,不可重复的数据 -->高中讲的"集合"

HashSet、LinkedHashSet、TreeSet

  1. Map接口:双列集合,用来存储一对(key--value)一对数据--》高中函数:y=f(x)

HashMap、LinkedHashMap、TreeMap、HashTable、Properties

3.0 Collection接口

3.0.1常用方法

  1. contains(Object obj):判断当前集合是否包含obj

要点:向Collection接口的实现类的对象中添加数据obj时,要求obj所在类重写equals()方法,因为contains方法要比较的是内容,而不是地址值

  1. retain(Collection coll):获取当前集合和coll集合的交集,并覆盖在当前集合上

  2. toArray():集合转数组,返回的是Object[]

那么数组如何转集合呢? 调用Arrays类的方法:Arrays.asList(可变形参数组)

  1. iterator():返回Iterator接口的实例,用于遍历集合元素。放在IteratorTest。java中测试

3.0.2 Collection接口的子接口List的实现类特点

不同:

  • ArrayList:作为List的主要实现类;线程不安全,但效率高;底层使用Object[] obj;
  • LinkedList:对于频繁的插入和删除操作,使用该类效率高;地底层使用链表存储
  • Vector:作为List接口的古老实现类;线程安全,但效率低;

相同:

  • 三类都实现了List接口,存储数据的特点相同:都是存储有序的,可重复的数据。

3.0.3 ArrayList源码分析

  1. jdk 7 情况下:
  • 在无参构造器中,底层是创建了长度为10的Object[]型数组elementDate;
  • 使用add()方法添加时:如果该次添加导致底层elementDate数容量不足,则扩容;
  • 默认情况下,扩容原来的容量的1.5倍,同时需要把原有的数组中的数据复制到新的数组中。

结论:建议开发中使用带参构造器:ArrayList list = 呢哇ArrayList(int capacity);避免扩容导致效率低.

  1. jdk 8 情况下:
  • 在无参构造器中,底层是Object[] elementDate初始化为{ },并没有创建长度;
  • 第一次使用add()方法添加时才底层创建长度为10的数组;
  • 后面思路与jdk 7一样

结论:jdk7的ArrayList的对象的创建类似但单例的饿汉式,而jdk8的ArrayList的对象的创建类似单例的懒汉式,延迟了数组的创建,节省空间。

3.0.4 List的常见方法


​ 前面我们已经了解Collection接口有两个子接口:List和Set接口;这里为什么只讨论List的方法呢?换言之是由List的特性决定的,我们知道List存储的特点是:存储有序,可重复的数据,所以我们可以通过指定的索引来操作数据。


增:add(Object obj);

删:remove(int index) //remove(Object obj);

改:set(int index,Object ele);

查:get(int index);

插入:add(int index,Object ele);

长度:size();

遍历:① Interator 迭代器方式;② 增强for循环; ③ 普通for循环。


难点:

@Test
public void test1(){
    ArrayList list = new ArrayList();
    list.add(1);
    list.add(2);
    list.add(3);
    list.add(4);
    //① Interator 迭代器方式;
    Iterator it = list.iterator();
    while (it.hasNext()){
        System.out.print(it.next());
    }
    //② 增强for循环;
    for ( Object obj:list){
        System.out.print(obj);
    }
    //③ 普通for循环。
}

3.0.5 Set接口理解

  1. 无序性:不等于随机性。存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值而决定的。
  2. 不可重复性:保证了添加的元素按照equals()方法不能返回true,即:相同元素只有一个。
  3. Set接口的所有方法都来自Collection接口。因为Set无序特点。

image-20221025135506598

  • LinkedHashSet:作为HashSet的子类;在添加数据时,还有next和p指针用于连接顺序添加时前后两个数据的位置;有了链表的结构,所以对于要频繁遍历的数据,采用LinkedHashSet比HashSet高效。

3.0.6 Map中的理解

--> HashMap:线程不安全,但效率高;Map主要实现类;可以存储null的键值对。

​ --> linkedHashMap:线程安全,但效率低;若需要频繁遍历,考虑该类;HashMap的子类。

--> TreeMap:按照添加顺序key-value键值对进行排序,实现排列遍历;考虑自然排序或定制排序。

​ -->Properties: 用来配置文件;key和value都是String类型。

HashMap底层:①jdk7:数组+链表; ②jdk8: 数组+链表+红黑树

面试题:

  • HashMap底层原理
  • HashMap和LinkedHashMap的区别

Map结构的理解:

  • key采用了set存储,数据无序且不重复,保证了key主键的唯一;value可以说采用Collection存储,数据无序可重复;Entry为key-value键值对对象,Entry无序不重复,采用set存储。
  1. 以HashMap为例:key所在的类需要重写equals()和hashCode()。

  2. 以TreeMap为例:自然排序:key所在类实现Comparable。

    ​ 定制排序:在实例化TreeMap时,使用有参构造器,参数为Comparator(),使用匿名内部类。

3.0.7HashMap底层源码分析

一. jdk 7 情况:

  • 在实例化过程中,底层创建了一个长度为16的数组Entry[]

  • 在调用put()方法后,底层key1先调用hashCode()计算哈希值,经过某种算法简化后确定存储位置

  • 如果该位置上没有元素,则直接添加;-->key1-value1 -----------------顺序存储

  • 如果有多个元素,这些元素通过链表存储,比较这些元素和key1的哈希值;

  • 如果哈希值不同,则直接添加;-->key1-value1------------ 链表存储

  • 如果哈希值不同,则key1与这些哈希值不同的元素用equals()比较;

  • 如果内容不同,则直接添加;-->key1-value1

  • 如果内容一样,则覆盖该数据。 --------------------链表存储

  • 扩容:为原来的2倍,并数据复制到新数组。

二. jdk 8 情况:

    1. new TreeMap() 没有创建长度为16的数组
    2. 只有在首次调用put()才初始化长度
    3. 存储:数组+链表+红黑树
    4. 当某一索引上以链表形式存储的数据个数大于8且数组长度为64时,使用红黑树存储链表上的数据便于查

4.0 元视图操作方法

  • 获取Map的键值对

三个方法:

  1. Set keySet(): 返回所有的key构成的Set集合。
  2. Collection values(): 放回所有的balues构成的Collection集合。
  3. Set entrySet():返回所有的key-value对构成的Set集合。
  • 通过以上方法调用后,就可以用Set(Collection)特有的迭代器遍历所有键值对。

9 泛型

1.0为什么有泛型

  • 泛型:Generic,用于指定类(或接口)某个属性需要返回值的参数类型;
  • 现在我们知道泛型就是控制类中某一属性的类型一致,如数组定义就说明了该数组应该存放什么样的类型元素一样;
  • 在集合中,在没有使用泛型的情况下,添加的元素类型不一,如果我们需要把该集合强转为某一类型集合输出时,会出现ClassCastException异常,导致强转失败。

2.0如何在集合中使用泛型

  • 使用尖括号,在尖括号里写需要统一定义的类型:类名<类型>();
  • 如果集合中数据为int型,则类型应该为Integer;
  • 该类型就叫泛型类型
  • 好处:如果比较两个元素大小时,假如没使用泛型,则逻辑上还需要强转类型。

3.0自定义泛型

  • 有哪些地方可以定义为泛型:类,接口,方法;
  1. 泛型方法:
public <E> List<E> copyFromArrayToList(E[] arr){

    //定义泛型方法时,在参数位置和返回值类型位置加上泛型符号<E>:只有当传入具体参数泛型类型时才确定
    //为了让编译器知道,在返回值前加上<E>代表是泛型。
    //具体代码
}
public class Father {
    public <E> List<E> copyFromArrayToList(E[] arr){

        ArrayList<E> list = new ArrayList<>();
        for (E e:arr){
            list.add(e);
        }
        return list;
    }
}
public class GenericTest {
    public static void main(String[] args) {
        Father father = new Father();

        Integer[] arr = new Integer[]{1,2,3,4};
        List<Integer> list = father.copyFromArrayToList(arr); //传入一个Object的数组
        System.out.println(list);
    }
}

代码输出: [1, 2, 3, 4]

总结:

  1. 如果将泛型方法声明为static,那么是可以的,因为该泛型方法的泛型参数是在调用方法时才确定的,不是在实例化类时确定。如果泛型参数确定,即数组确定,即实例化类确定,这个生命周期短于静态方法的生命周期。
  2. 子类在继承泛型父类时,子类也要声明为泛型,类型可以不同,然后在实例化时就可以不用声明为泛型了。

4.0泛型在继承上的表现

5.0通配符的使用

6.0泛型应用举例(急可不读)

10反射

10.1使用ClassLoader加载配置文件

  • 前面学习了使用集合Map接口的Properties加载配置文件
  • 这里我们将演示两种加载配置文件的方式
  • 我们将配置文件写在src源文件下,如图:image-20221029121050200

方式一:使用Properties加载

@Test
public void Test1() throws IOException {
    //方式一:
    Properties properties = new Properties();
    FileInputStream file = new FileInputStream("src\\JDBC.properties");
    properties.load(file);


    String user = properties.getProperty("USER");
    String password = properties.getProperty("password");
    System.out.println("user:"+user+"password:"+password);
}

方式二:使用当前类的ClassLoader加载器加载配置文件

public class PropertiesTest {
    @Test
    public void Test1() throws IOException {
        Properties properties = new Properties();
        //1. 当前类.class.getClassLoader()获取一个ClassLoader对象
        ClassLoader classLoader = PropertiesTest.class.getClassLoader();
        //2.ClassLoader对象.getResourceAsStream("配置文件路径")
        InputStream is = classLoader.getResourceAsStream("JDBC.properties");
        properties.load(is);

        String user = properties.getProperty("USER");
        String password = properties.getProperty("PASSWORD");
        System.out.println("user:"+user+"password:"+password);

    }
}

总结:加载配置文件的两种方式都需要Properties类的Load()加载方法加载流,即加载配置文件;二者不同的地方就是:方式二类加载器默认路径是src源目录下的,可以不用写src//

10.2创建运行时类对象

  • 任何类的Class类种有个方法newInstance()可以创建一个运行时类对象;
  • 调用此方法后,创建对应的运行时类对象,内部调用了运行时类的空参构造器;
  • 所以,要想正常创建运行时类对象,需要满足以下几点要求:
    1. 运行时类必须提供空参构造器
    2. 空参构造器的访问权限要足够大

在javabean种要求提供一个puvlic的空参构造器,原因如下:

  1. 便于使用反射,创建运行时类对象
  2. 便于子类继承父类该运行时类时,默认调用super()时,保证父类有该构造器。

10.3调用运行时类的指定属性

  1. Class clazz = 类名.class;

  2. 创建运行时类对象具体类名 a = clazz.newInstance()

  3. 获取运行时类的指定变量名的属性Type b = clazz.getDeclaredField(String name)

  4. 将属性暴力反射b.setAccessible(true)

  5. 获取、设置指定对象的属性值b.set(运行时类对象a,"具体值") b.get(运行时类对象a)

10.4调用运行时类的指定方法

非静态方法:

  1. Class clazz = 类名.class;

  2. 创建运行时类对象具体类名 a =(具体类名)clazz.newInstance()

  3. 获取指定的某个方法 Method b= clazz.getDeclaredMethod("指明获取的方法的名称",Class类的形参列表)

  4. 将方法暴力反射b.setAccessible(true)

  5. 调用方法的b.invoke(运行时类对象,”实参列表“)

  6. 方法的Invoke要返回的是原方法的返回值。

  7. Class类的形参列表:参数类型.class

静态方法:

b.invoke()括号里可以是任何对象,因为本来Class类就知道堆里就有静态方法了;

11技巧

11.1 Debug中sept into 失灵的解决

  • 点开File->setting->Build,Exception,Deployment->Debugger->Stepping->Do not step into the classes里面的java.和javax.前面的勾勾取消掉,然后就可以进去了。

方法2

  • 重新安装idea,然后再设置jdk时,把外部的jre也安装上
posted @ 2022-11-15 19:55  郭培鑫同学  阅读(24)  评论(0编辑  收藏  举报