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)分开工具类和测试类
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.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)
这一思想体现了封装性思想。
- 封装性的体现:
-
- 类的属性私有化(private),提供公有的(public)方法获取(getXXX)和设置(setXXX)
- 私有方法,仅供本类使用
- 单例模式....................
-
1.18 构造器
- 作用:
-
创建对象 People p = new Peopel();
-
初始化对象的信息 People p = new Peopel(Tom);
- 说明使用:
- 如果没有显示定义类的构造器的话,则默认提供一个带空参的构造器,开发中,如果需要有参构造器,则习惯提供一个空参构造器
- 定义一个构造器的方法:权限修饰符 类名(形参列表)
- 如果类中定义多个构造器,则构成重载。
- 一旦我们使用带参构造器,则需要我们自己提供一个空参的构造器。
- 一个类中,至少有一个构造器
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设计模式
个人理解:三层分别为视图层、服务层、数据层。
2. 面向对象(中)
2.1 继承性
- 格式:class A extend B{}
- 体现,一旦子类A继承父类B后,子类A中就获得了父类中声明的所有的属性和方法;
- 特别的,父类中有private权限声明的属性或方法,子类继承父类后,仍然认为获取了父类中的私有结构;
- 只是因为封装性的影响,使得子类不能直接调用父类的结构而已,换句话说,子类没有权限使用父类声明为private的属性或方法而已。
2.2 重写方法
面试题:重载与重写的区别:
-
重载:
- 方法名必须一致
- 方法修饰权限和返回值类型不受限制
- 只要形参位置的类型不一样,就构成重载:
- 可能情况1:两个方法中的形参一样,但位置不同;
- 可能情况2:形参个数不同;
-
重写:
- 重写父类同名同参方法,修改父类的内容
- 所以重写方法必须与父类同名同
- 概念:子类继承父类后,可以对跟父类同名同参的方法进行覆盖。(好比煎饼)
- 方法的声明: 权限修饰符 返回值类型 方法名(形参列表){}
- 规定: ①父类中同参同名方法; ②子类中同参同名方法
- ①的权限修饰符为private,②不能有重写
- ②的权限修饰符必须大于等于①,即子类的权限更大
- ①的返回值类型为void,②也必须只能为void
- ①的返回值类型为基本数据类型,②也必须为基本数据类型
- ①的返回值类型为A类型,②可以是A类型,也可以是A类型的子类
- ①②要么都声明为非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:
- 调用当前方法的属性或方法
- this关键字从子类中的属性或方法查找
- 子类的空参构造器默认调用
super();
访问父类的空参构造器
super:
- 调用父类方法的属性或方法
- super关键字从父类中查找
相同点:
- 在构造器里面,
this(形参列表);
或super(形参列表);
只能放在首行 this(形参列表);
和super(形参列表);
只能出现一个,才能满足1
2.5子类对象的实例化
- 当我们用子类的空参构造器创建对象时,系统默认使用
super()
直接或间接调用父类的构造器,进而形成一层又一层子类调用父类构造器,直到我们调用包java.lang.Object
中的空参构造器。
2.6多态性
使用前提:
- 类的继承关系
- 方法的重写
多态理解:
- 父类的引用指向子类的对象
- 编译,看左边;运行,看右边。
例如:People p1 = new boy();
- 编译时看父类中方法,运行时看子类中重写方法
2.7向下转型
编译时看父类方法,所以对象实例化后,如果子类中的方法除了与重写父类方法外,还有子类特有的方法时,那么该实例化对象是无法调用子类特有的方法的;但是,内存上实际是有加载进子类特有的属性和方法的,只是因为该实例化对象声明为父类类型People
;解决办法就是对实例化这一过程进行向下转型。
如何向下转型呢?在基本数据类型中,如果我们想将高级数据类型double
转为低级int
,需要使用强制类型转换符。double d; int i =(int) d;
在基本数据类型中,强转型会导致精度损失;在多态实例化过程中,也可能出现异常。
引入关键字instanceof
- 使用:a instanceof A :如果对象a是类A的实例,返回true;否则false。
- 在向下转型前,用于判断要不要进行转型。
2.8 ==与equals的区别
==
运算符
-
可以使用在基本数据类型和引用数据类型中
-
如果比较的是基本数据类型变量,比较的是两个变量所保存的数据是否相等,这里不一定要数据类型一样
-
如果比较的是引用数据类型变量,比较两个变量的地址是否相等,即两个引用变量是否指向同一个对象实体
特别的
注意:==
运算符使用时,必须保证符号两边的变量类型一致;
equas()
方法
- 只适用于引用数据类型
- Object类中
equals()
的定义:public boolean equals(Object obj){ return (this==obj);}
说明:Object类中的equals()方法与==运算符作用相同,都是比较的是两个对象的地址值是否相等 - 而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可以修饰:属性,方法,代码块,内部类
属性:
- 按是否使用
static
修饰,分为静态属性(类变量)vs非静态属性(实例变量)
实例变量:创建多个对象时,多个对象设置同一个属性时,不会导致另一个对象的属性修改
静态变量:创建多个对象是,多个对象共享同一个静态变量,当通过某一对象修改静态变量值时,会导致其他对象调用该静态变量时,是已经修改后的值了。
- 静态变量随着类的加载而加载,可以通过
类.静态变量
的方式调用; - 静态变量的加载要早于实例变量的创建;
- 由于类只会加载一次,所以静态变量在内存中也只有一份,存在于方法的静态域。
方法:
- 静态方法vs普通方法
- 静态方法,只能调用静态方法或属性;普通方法,都可以调用任何方法和属性,但开发中没必要用static修饰的静态方法和属性。
- 静态方法和属性的使用,一般是从类的生命周期去理解的。随着类的加载,静态方法和属性存在,静态方法早于普通方法,如果要使用到静态方法,一般是用
类.静态方法
方式使用。 - 开发中,什么情况下需要使用到
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; } }
二:单例设计模式懒汉式实现
- 私有化构造器
- 声明私有静态对象属性
- 提供静态getXxx()方法返回对象属性
饿汉式模式vs懒汉式模式
饿汉式:好处:线程安全; 坏处:对象的加载时间拉长
懒汉式:好处:延长对象创建; 坏处:存在线程不安全..........todo
3.3 抽象类abstract
- abstract修饰的类:抽象类
- 该类不能实例化创建对象
- 抽象类中一定有构造器,便于子类实例化调用
- 开发中,都会提供抽象类的子类,让子类实例化,完成相关的操作
- 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
- 接口的理解
Java开发中,接口通过让类去实现(implements)的方法来使用
如果实现类覆盖了接口中的所有抽象方法,则该实现类可以实例化对象
如果实现类没有覆盖接口全部抽象方法,则该类为抽象类。
子类是我,接口是师傅,父类是父亲,师傅传武功我,父亲传基因于我。
- 接口的使用:
- 满足多态性
- 是一种规范
- 面向接口编程
- 接口的匿名实现类的匿名对象:
USB类为接口,Flash类和Printer为接口的实现类
在main主方法中,对Computer类实例化对象c,对象c调用Computer类中的成员方法,实参匿名化
格式
c.成员方法(new USB(){重写接口所有抽象方法}))
"new USB(){重写接口所有抽象方法})"就是接口的匿名实现类的匿名对象
3.7 接口应用:代理模式
两个接口实现类通过接口可以把实现类分为代理类和被代理类
3.8接口应用:工厂模式
创建对象给工厂做,在main方法中分离创建对象和调用对象
抽象类和接口的区别:
- 都无法实例化
- 都是抽象的
- 类与类,单继承;接口与接口多继承;接口与类,多实现
- 再说明一下抽象类的定义和接口的定义
4.异常处理
个人理解:当我们打游戏时,网络掉线,提示:没联网,游戏退出。这就是对异常进行处理,异常部分直接运行时直接略过,保证整个游戏因掉线不会奔溃。
- try{}catch(){}finally{}:真正处理掉异常的包起来的代码,finally可有可无,若有,则说明最后一部分无论前面是否有异常,我都会执行finally里面的代码。
- throws+异常类型:如果有异常,则抛出异常,给其他类处理,异常代码后的所有代码都不会执行到。
- 开发中,如何选择异常处理:
- 情景一:如果父类中出现异常了,没有用throws抛出,则子类也不能用throws抛出,必须用try{}catch(){}。
- 情景二:方法中嵌套了方法,在main主方法中调用最外层方法,用try{}catch{}处理异常;有递进关系的方法都抛出异常,即用throws+异常类型。这样可以简洁代码。
- 手动抛出异常
格式:throw new 异常类型
。异常类型常用:RuntimeException(message)
或Exception(message)
在可能出现异常的地方手动添加throw ,生产一个异常类型;我们引入异常类型,还要在类中用throws+异常类型或者try—catch的方式抓异常
项目2:《开发团队调度软件》
涉及知识:
问题:
- 为什么在Status类中要重写toString?
难点:
- 角色的不同需要使用不同的设备,封装成方法,当角色分成三类时,分别使用到方法的形参。
- 初始化数组,索引位置的选择。
5.多线程
有趣知识
以前cpu是单核的,目前有c盘,d盘,e盘,c盘有一份数据。问:同时将c盘数据复制到d盘一份,e盘一份,还是先将c盘数据复制到d盘好了后再c盘数据复制到e盘,这两种方式哪种传输数据更快些?
1. jdk5.0之前两种创建多线程
一:第一种创建多线程方法Thread
- 创建Thread的子类
- 重写Thread类的
run()
方法 - main方法,创建子类对象并
对象.start()
start方法作用:
- 启动线程当前对象的线程
- 调用当前线程的run方法
如何创建多个线程:创建多个对象,并start开启线程。
或者使用匿名内部类形式
main{ new Thread(){ @Override public void run(){ //具体要执行的代码} }.start(); }
二:第二种创建多线程方式实现Runnable接口
- 创建一个实现Runnable接口的类
- 实现类重写Runnable接口的run方法
- 创建实现类对象
- 把实现类对象作为Thread构造器参数
- 调用start方法
2. 多线程中常见方法使用
Thread中的常见方法 | 主要功能 |
---|---|
start() | 启动线程;调用run() |
run() | 线程要执行的内容放此方法体内 |
yield() | 释放当前线程 |
currentThread() | 静态方法,返回当前线程名称 “Thread-1:” |
setName() | 设置线程名称 |
getThread() | 获取当前线程名字 |
join() | 主动插队抢占cpu资源直到当前线程终结死亡 |
sleep(long milliTime) | 指定当前线程睡眠多少毫秒。在指定时间内当前线程是阻塞状态 |
isAlive() | 是否还存活着 |
修改主线程名字的方式:
currentThread.getName("");
- 在Thread的子类中提供带参构造器,参数类型:String
修改其他线程名字的方法:
currentThread.getName("");
对象.getName("");
- 在Thread的子类中提供带参构造器,参数类型:String
3. 线程优先级的设置
-
setPriority(int p) getPriority()
-
参数p的取值:
Max_PRIORITY
:10MIN_PRIORITY
:1NORM_PRIORITY
:5 -
高优先级的线程要抢占有低优先级线程cpu的执行权,但是只是从高概率上讲,并不意味着高线程执行完后低线程才执行
4. 多线程的卖票问题
问题描述:有三个窗口,一个卖票员手里有100张票,游客买票。
初步理解:三个窗口代表着三线程,游客们分别在三窗口排队买票,因为总共才100张票,所以设置变量为100,并且因为三个窗口是共享100张票的,所以应该共享。
代码理解:实例化三个线程时,Java虚拟机的堆会同时创建三份 同名属性。但,我们只有一份100张票,故三个对象只能共享,所以使用static关键字声明属性。
5. 同步线程安全问题
方式一:同步代码块
- 格式:
synchronized(同步监听器){ //需要被同步的代码。}
- 说明:需要共享数据的代码即为需要被同步的代码;共享数据就是多个线程对象共同操作的属性变量;同步监听器即为锁;任何对象都可充当一把锁
- 情况:多个线程独享需用一把锁操作共享数据。
如何获得锁?
方式一:this ,当前对象
方式二:当前类.class() ,类加载只有一次
方式三:new一个任意对象
方式二:同步方法
- 使用:用关键字声明synchronized
继承Thread类的多线程:因为实例化后,Thread子类随着加载,子类对象同时创建三个线程调用属性变量,在堆空间会有三分属性变量,我们只想要对象使用同一个属性变量,因此我们可以将属性声明为static类型的。
实现Runnable接口的多线程:将需要同步的代码封装成方法
- 说明:
- 如果同步方法为静态,那么当前类的锁是:当前类.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,店长让生产 者等待。
- 分析:
-
- 是否为多线程问题? 是,有生产者线程和消费者线程
- 是否有共享数据? 是,店长和产品数量
- 怎么解决线程安全问题? 同步机制,有三种方式
- 是否涉及线程的通信? 是,需要使用wait()方法等待
- 步骤:
- 使用继承Thread多线程
- 创建店长类
- 创建生产者类,将店长类作为属性变量,并提供有参构造器,参数为店长类对象
- 创建消费者类,将店长类作为属性变量,并提供有参构造器,参数为店长类对象
- 生产者类继承Thread,并重写run方法
- 消费者类继承Thread,并重写run方法
- 在店长类中,声明产品数量,一开始为0个;定义生产产品和消费产品的方法
- 在生产方法中,如果产品大于20,不再生产,让生产者等待
- 在消费方法中,如果没有产品,让消费者等待
- run方法调用店长类中的方法
- 在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接口的方式
- 创建一个实现Callable接口的实现类
- 实现call方法,把线程需要操作声明在call()方法中
- 创建Callable接口的实现类对象
- 创建FutureTask类对象,并将Callable接口的实现类对象作为参数传递给FutureTask类
- 将FutureTask类对象作为参数传递给Thread类,Thread调用start()启动线程
- 用FutureTask特有方法get(),获取call()方法中的返回值
get()返回值:是FutureTask构造器参数Callable实现类重写的call()方法中的返回值。
使用实现Callable接口的优势:
- 有返回值,可以提供给其他线程使用
- 可以抛异常,方便其他线程知道该线程有异常要处理
- 使用了泛型.........
四:使用线程池的方式
背景:经常创建和销毁,使用量特大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建多个线程,放在线程池中,使用时直接获取,使用完后放回线程池
好处:提高响应速度;减少资源消耗;便于线程管理。
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAlive Time:线程没有任务时最多保持多长时间后会终止
步骤:
- 创建池 ExecutorService service = Executors.newFixedThreadPool(n);
- 调用方法,参数为Runable接口实现类或Callable接口实习类 execute()、submit()
- 关闭池 shutdown()
问题:在其他方式创建多线程中,都可以设置线程属性,这里ExecutorService是接口,无法设置,因为接口抽象的,一般交给接口实现类完成具体操作,那么如何获取ExecutorService接口的实现类呢?
解决:向下转型,这里把接口转为实现类ThreadToolExecutor,再调用实现类ThreadToolExecutor中的setCorePoolSize() setkeepAliveTime().
6. 枚举类
- 自定义
1.0使用enum关键字
步骤:
- enum代替class声明类
- 常量名(对象属性实参)
- 声明对象属性:private final 修饰
- 私有化构造器,提供带参
- 提供其他诉求:提供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关键字定义的枚举类实现接口
- 实现接口
- 在常量名重写接口方法
7.注解 (Annotation)
1.0概述
- jdk5.0新增特性
- 其实就是代码里的特殊标记,这些标记可以在编译,类加载,运行时被读取,并保证程序员可以不改变原有逻辑的情况下,在源文件中嵌入一些补充信息。
- 在JavaEE中,注解占据了重要角色,例如用来配置应用程序的任何切面,代替JavaEE旧版本所遗留的繁冗代码和XML配置等。
- JDK内置的注解:@Overrde @Deprecated @SuppressWarnings
2.0元注解
如何自定义注解:参照JDK内置注解定义
- 注解声明为:@interface
- 部定义成员,通常使用value表示
- 可以指定成员的默认值,使用default定义
- 如果自定义注解没有成员,表明是一个标识作用
注意点:
- 如果注解有成员,在使用注解时,需要指明成员的值。
- 自定义注解必须配上注解的信息处理流程(如反射)才有意义
- 之定义注解通常都会指明两个元注解:Retention,Target
- jdk提供的4种元注解:元注解就是对现有的注解进行解释说明的注解
Retention:指定所修饰的注解的生命周期:SOURCE\CLASS\RUNTIME
Target:用于指定被修饰的注释能用于修饰哪些程序元素
Documented:所修饰的注解会在javadoc解析时被保存下来
Inherited:继承性
- 通过反射机制获取注解信息.....................
3.0 jdk 8 注解新特性
- 可重复注解
- 在MyAnnotation上声明@Repeatable,成员值为MyAnnotation.class.
- MyAnnotation的Target与Retention与Inherited和MyAnnotation相同。
- 类型注解
- ElementType.TYPE_PARAMETER 表示该注解能写在类型变量的声明语句中(如:泛型声明)。
- ElementType.TYPE_USE 表明该注解能写在使用类型的任何语句中。
8.集合
1.0概述
- 集合和数组都是对多个数据进行存储操作的结构,简称Java容器。
存储:主要指的是内存层面的存储,不涉及到持久化的存储(.txt,jpg,avi,数据库)
- 数组在存储多个数据方面的特点:
- 一旦初始化后,长度确定,类型也确定
String[] arr; int[] arr; Object[] arr;
- 数组缺点:
- 长度不可修改
- 数组提供的方法有限,对于增删改等操作很不方便,效率也不高
- 在获取数组中实际元素的个数的需求上,没有相应的属性和方法可用
- 数组存储数据的特点:有序,可重复,对于无序和不重复的需求,不能满足
2.0框架
- Collection接口:单例集合,用来存储一个一个的对象
- List:存储有序,可重复的数据 -->"动态"数组
ArrayList、LinkedList、Vector
- Set:存储无序,不可重复的数据 -->高中讲的"集合"
HashSet、LinkedHashSet、TreeSet
- Map接口:双列集合,用来存储一对(key--value)一对数据--》高中函数:y=f(x)
HashMap、LinkedHashMap、TreeMap、HashTable、Properties
3.0 Collection接口
3.0.1常用方法
- contains(Object obj):判断当前集合是否包含obj
要点:向Collection接口的实现类的对象中添加数据obj时,要求obj所在类重写equals()方法,因为contains方法要比较的是内容,而不是地址值
-
retain(Collection coll):获取当前集合和coll集合的交集,并覆盖在当前集合上
-
toArray():集合转数组,返回的是Object[]
那么数组如何转集合呢? 调用Arrays类的方法:Arrays.asList(可变形参数组)
- iterator():返回Iterator接口的实例,用于遍历集合元素。放在IteratorTest。java中测试
3.0.2 Collection接口的子接口List的实现类特点
不同:
- ArrayList:作为List的主要实现类;线程不安全,但效率高;底层使用Object[] obj;
- LinkedList:对于频繁的插入和删除操作,使用该类效率高;地底层使用链表存储
- Vector:作为List接口的古老实现类;线程安全,但效率低;
相同:
- 三类都实现了List接口,存储数据的特点相同:都是存储有序的,可重复的数据。
3.0.3 ArrayList源码分析
- jdk 7 情况下:
- 在无参构造器中,底层是创建了长度为10的Object[]型数组elementDate;
- 使用add()方法添加时:如果该次添加导致底层elementDate数容量不足,则扩容;
- 默认情况下,扩容原来的容量的1.5倍,同时需要把原有的数组中的数据复制到新的数组中。
结论:建议开发中使用带参构造器:ArrayList list = 呢哇ArrayList(int capacity);避免扩容导致效率低.
- 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接口理解
- 无序性:不等于随机性。存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值而决定的。
- 不可重复性:保证了添加的元素按照equals()方法不能返回true,即:相同元素只有一个。
- Set接口的所有方法都来自Collection接口。因为Set无序特点。
- 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存储。
-
以HashMap为例:key所在的类需要重写equals()和hashCode()。
-
以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 情况:
-
- new TreeMap() 没有创建长度为16的数组
- 只有在首次调用put()才初始化长度
- 存储:数组+链表+红黑树
- 当某一索引上以链表形式存储的数据个数大于8且数组长度为64时,使用红黑树存储链表上的数据便于查
4.0 元视图操作方法
- 获取Map的键值对
三个方法:
- Set keySet(): 返回所有的key构成的Set集合。
- Collection values(): 放回所有的balues构成的Collection集合。
- Set entrySet():返回所有的key-value对构成的Set集合。
- 通过以上方法调用后,就可以用Set(Collection)特有的迭代器遍历所有键值对。
9 泛型
1.0为什么有泛型
- 泛型:Generic,用于指定类(或接口)某个属性需要返回值的参数类型;
- 现在我们知道泛型就是控制类中某一属性的类型一致,如数组定义就说明了该数组应该存放什么样的类型元素一样;
- 在集合中,在没有使用泛型的情况下,添加的元素类型不一,如果我们需要把该集合强转为某一类型集合输出时,会出现ClassCastException异常,导致强转失败。
2.0如何在集合中使用泛型
- 使用尖括号,在尖括号里写需要统一定义的类型:类名<类型>();
- 如果集合中数据为int型,则类型应该为Integer;
- 该类型就叫泛型类型
- 好处:如果比较两个元素大小时,假如没使用泛型,则逻辑上还需要强转类型。
3.0自定义泛型
- 有哪些地方可以定义为泛型:类,接口,方法;
- 泛型方法:
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]
总结:
- 如果将泛型方法声明为static,那么是可以的,因为该泛型方法的泛型参数是在调用方法时才确定的,不是在实例化类时确定。如果泛型参数确定,即数组确定,即实例化类确定,这个生命周期短于静态方法的生命周期。
- 子类在继承泛型父类时,子类也要声明为泛型,类型可以不同,然后在实例化时就可以不用声明为泛型了。
4.0泛型在继承上的表现
5.0通配符的使用
6.0泛型应用举例(急可不读)
10反射
10.1使用ClassLoader加载配置文件
- 前面学习了使用集合Map接口的Properties加载配置文件
- 这里我们将演示两种加载配置文件的方式
- 我们将配置文件写在src源文件下,如图:
方式一:使用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()
可以创建一个运行时类对象; - 调用此方法后,创建对应的运行时类对象,内部调用了运行时类的空参构造器;
- 所以,要想正常创建运行时类对象,需要满足以下几点要求:
-
- 运行时类必须提供空参构造器
- 空参构造器的访问权限要足够大
在javabean种要求提供一个puvlic的空参构造器,原因如下:
- 便于使用反射,创建运行时类对象
- 便于子类继承父类该运行时类时,默认调用super()时,保证父类有该构造器。
10.3调用运行时类的指定属性
-
Class clazz = 类名.class;
-
创建运行时类对象
具体类名 a = clazz.newInstance()
-
获取运行时类的指定变量名的属性
Type b = clazz.getDeclaredField(String name)
-
将属性暴力反射
b.setAccessible(true)
-
获取、设置指定对象的属性值
b.set(运行时类对象a,"具体值")
b.get(运行时类对象a)
10.4调用运行时类的指定方法
非静态方法:
-
Class clazz = 类名.class;
-
创建运行时类对象
具体类名 a =(具体类名)clazz.newInstance()
-
获取指定的某个方法
Method b= clazz.getDeclaredMethod("指明获取的方法的名称",Class类的形参列表)
-
将方法暴力反射
b.setAccessible(true)
-
调用方法的
b.invoke(运行时类对象,”实参列表“)
-
方法的Invoke要返回的是原方法的返回值。
-
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也安装上
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)