Java基础复习
Java常用快捷键
删除指定行:Ctrl+D
复制当前行到下一行:Ctrl+Alt+向下光标
运行代码:Alt+B
查看类的继承关系:把光标放在类名上,Ctrl+H
补全代码:Alt+/
注释:Ctrl+/
位运算
原码、反码、补码
- 二进制的最高位是符号位,0表示整数,1表示负数
- 正数的原码,反码,补码都一样(三码合一)
- 负数的反码 = 原码的符号不变,其他位取反
- 负数的补码 = 反码 + 1,负数的反码 = 补码 - 1
- 0的反码,补码都是0
- java没有无符号数,即java中的数都是有符号的
- 在计算机运行时,都是以补码的方式运行的
- 当看运行结果时,要看它的原码
位运算符
- &(按位与)与运算
- |(按位或)或运算
- ^(按位异或):两位一个为0,一个为1,结果为1,否则为0
- ~(按位取反):取反操作
>>
(算术右移):低位溢出,符号位不变,并用符号位补溢出的高位(相当于/2)- <<(算术左移):符号位不变,低位补零(相当于*2)
>>>
(无符号右移):低位溢出高位补零
数组
数组的定义
//动态初始化//方式一int[] arr1= new int[5];//方式二int[] arr2;arr2 = new int[10];//静态初始化int[] a = {1,2,3,4,5};
基本数据类型
char
- char的本质是一个整数,在输出时,输出的是unicode编码对应的字符,如果想要直接输出字符对应的数字,可以进行类型转换
char a='A';System.out.println((int)a)
- char类型是可以运算的,相当于一个整数
自动类型转换
转换规则:
char->int->long->float->doublebyte->short->int->long->float->double
细节:
-
当多种数据类型混合运算时,系统首先将所有数据类型转换为容量最大的数据类型,然后进行计算
int a=1;float f = a + 1.1; //错误,1.1为double类型float f = a + 1.1F; //正确
-
不可以把容量大的数据类型赋值给容量小的数据类型
-
将一个具体的数字赋给byte类型,只要数字在-128~127就可以
-
byte,short和char之间可以进行计算,在计算时首先转换为int类型(赋值操作先对等号右边进行处理)
byte b1=1,b2=2;byte b3=b2 + b1; //这样是错误的,只要有byte类型,计算时就会转换为int在计算
-
boolean不参与类型转换
-
byte和short都不能和char进行自动类型转换
-
c++和java中区别
int a = 20;char b = 'A';int c = b + a; //85char d = b + 20; //U 在c++中这段代码是正确的int a = 20;char b = 'A';int c = b + a; //85char d = b + 20; //U 在java中就会报错,需要进行强制类型转换
强制类型转换
当想要把一个容量大的数据类型赋值给容量小的数据类型,就要进行强制类型转换,会有精度损失
细节:
- 强制转换符号针对最近的操作符有效,往往需要小括号提升优先级
- char可以保存int类型的常量值,但不能保存int类型的变量
基本数据类型->String
在基本数据类型后加一个字符串就可以实现
int a=10;//方式一String str = a + "";//fstr = String.valueOf(a);
String->基本数据类型
使用包装类提供的方法实现转换
String str = "123";int a = integer.parseInt(str);
String->char
使用String类的charAt()
来获取字符串中的字符
字符编码表
- ASCII:表用一个字节,实际上一个字节可以表示256个字符,只用了128个
- Unicode:固定大小的编码,兼容ASCII码,使用两个字节表示字符,字母和汉字统一用两个字节表示(这样会浪费空间)
- UTF-8:大小可变的编码表,字母用一个字节,汉字用三个字节。在互联网中使用最广的对Unicode的改进。
- GBK:可以表示汉字,字母使用一个字节,汉字使用两个字节
- GB2312:可以表示汉字(用的少)
- BIG5:繁体中文,台湾,香港
属性
属性,也可以叫做字段,成员变量。属性如果未赋值默认值为:
- byte 0
- short 0
- int 0
- long 0
- float 0.0
- double 0.0
- char \u000
- boolean false
this指针
-
在每创建一个对象的同时,就会有一个this指针指向当前的对象。所以每创建十个对象就会有十个this指针指向各自的对象
-
this在类中可以用来访问该类的成员,包括成员变量,成员函数,构造函数。
Static关键字
static可以修饰:变量,代码块,方法(不能修饰构造方法)
- 静态变量:引用类型初始值为null,基本数据类型初始值为1.(初始值和普通变量相同)
- 静态代码块
- 静态方法:
- 可以访问静态变量,静态方法。
- 不可以访问非静态变量,非静态方法。
- 不能使用与类对象相关的关键字(this,super)
静态代码块:static修饰代码块,属于类。类加载一次,静态代码块执行一次。需要注意的是:因为代码加载顺序的问题,**静态代码块,是不能写在静态方法内部的,只能写在类的内部。同理,静态方法内部也是不能定义静态变量的。**也就是说静态内部不能再有静态
成员代码块:属于对象实例。调用一次构造方法创建一个对象,成员代码块执行一次。先执行成员代码块,再执行构造方法
局部代码块:属于方法,执行一次方法,局部代码块执行一次。方法中的代码顺序执行,不会提前执行。
main()
main()
有虚拟机调用- 因为虚拟机要调用类中的main(),所以一定要public修饰
- Java虚拟机在执行main()时,不必创建对象就可以调用,所以要用static修饰
- main()的参数是一个String类型的数组,该数组中保存执行java命令时,传递给所运行类的参数
访问权限修饰符
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wZlu1fCP-1632385272609)(D:\document\小林code\小林图片\Java访问权限修饰符.png)]
- 权限修饰符可以修饰构造方法
- 类只能使用public和默认修饰符
继承
继承的基本概念:
(1)Java不支持多继承,也就是说子类至多只能有一个父类。
(2)子类继承了其父类中不是私有的成员变量和成员方法,作为自己的成员变量和方法。
(3)子类中定义的成员变量和父类中定义的成员变量相同时,子类中会有两个同名的变量.
(4)子类中定义的成员方法,并且这个方法的名字,返回类型,以及参数个数和类型与父类的某个成员方法完全相同,则父类的成员方法不能被继承,使用自己的方法。
继承中的成员变量:
- 子类直接继承父类非私有的成员变量,作为自己的变量
- 子类继承了父类所有的方法和属性,但是父类中私有的属性和方法在子类中不能直接访问,要通过父类公共的方法来访问
- 子类如果有和父类完全一样的变量:子类中会有两个完全一样的变量
继承的使用细节:
- 子类必须调用父类的构造器,来完成父类的初始化。
- super使用时,必须放在构造器的第一行(super只能在构造器中使用)。
- this和super,都必须放在构造器的第一行,因此两个方法方法不能在一个构造器中使用
- 父类构造器的调用不限于直接父类,会一直追随到Object类(顶级类)
- 不能滥用继承,父类和子类必须符合is-a的关系
- A继承B,B继承C,可实现类似A继承B,C的效果
- 在继承体系中,按照查找关系来放回属性值:
- 如果子类中有该属性,就直接放回
- 如果子类中没有该属性,就到父类中找(如果父类中有该属性,并且可以访问,就返回)
- 一直早到Object类,如果还没找到就会报错
- 在查找的过程中,如果找到了该属性,但该类中的该属性时不可访问的,就会直接报错,不会继续往上找
super关键字的作用:super代表父类的引用,可以访问父类非私有的属性,方法和构造器
super的使用细节:
- 调用父类构造器的好处(分工明确,父类初始化父类的属性,子类初始化子类的属性)
- 当子类和父类中有重名的成员(属性和方法),为了访问父类的成员必须通过super。如果没有重名时,使用super,this,直接访问的效果一样
继承体系中的类加载机制:
- 先初始化父类,在初始化子类。即先调用父类构造器,在调用子类构造器
继承中属性和方法访问机制
- 当要访问一个方法或属性时,先在本类中找,找了就返回结果,如果没有找到的话,就到父类中找,如果找到就返回,一直找到Object顶级类。在这个过程中,只要找到可返回的属性或方法就会返回结果,不再继续往下找。如果都没找到就回报错。
- 可以使用super关键字,重当前类的父类的开始找(super关键字的作用)。
- 查找属性和方法唯一不同的点在于:属性在堆中查找,方法在方法中查找
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xVb02PDe-1632385272611)(D:\document\小林code\小林图片\继承内存关系图.png)]
编译阶段的过程:
-
程序员需要在硬盘的某个位置新建一个.Java扩展名的文件,该文件被称为Java源文件,源文件当中编写的是Java源代码/源程序,而这个源程序是不能随意编写的,必须符合Java语法规则
-
Java程序员需要使用
JDK
当中自带的Javac.exe
命令进行Java程序的编译。一个Java源文件可以编译生成多个.class文件(字节码文件)
运行阶段的过程:
1. 打开DOS命令窗口,输入Java A ,Java.exe
命令会启动Java虚拟机(JVM
),JVM
会启动类加载器classLoader
,classLoader
会去硬盘上搜索A.class
文件.
2. 找到该文件将该字节码文件装载到JVM
中,JVM
将.class字节码文件解释成二进制形式,然后操作系统执行二进制和底层硬件平台进行交互.
属性和成员变量的区别
属性的官方定义:SUN官方定义为属性是指get或者set方法名 去掉get或者set后,把剩余的部分首字母改为小写后,即为这个类的属性。
大多数情况下,成员变量和属性的概念是一样的。
运行时类型和编译类型
Java的引用变量有两个类型,一个是编译时类型,一个是运行时类型,编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的类型决定。如果编译时类型和运行时类型不一致,会出现所谓的多态。因为子类其实是一种特殊的父类,因此java
允许把一个子类对象直接赋值给一个父类引用变量,无须任何类型转换,或者被称为向上转型,由系统自动完成。
// 编译时类型 运行时类型 Animal cat=new Cat();
引用变量在编译阶段只能调用其编译时类型所具有的方法,但运行时则执行它运行时类型所具有的方法(意思是说:编写代码时,只能调用父类中具有的方法,如果子类重写了该方法,运行时实际调用的是运行时类型的该方法。程序在编译时,会在编译类型中检查是否具有所调用的方法,如果编写代码时,使用引用变量调用子类中的特有方法,或者调用重载了父类中的方法,而父类中找不到该方法,则会报编译错误),因此,编写Java代码时,引用变量只能调用声明该变量所用类里包含的方法。与方法不同的是,对象的属性则不具备多态性。通过引用变量来访问其包含的实例属性时,系统总是试图访问它编译时类所定义的属性,而不是它运行时所定义的属性。所以,要访问子类中特有的方法和属性,在编写代码时,则必须进行类型转换。
代码演示:
class Father{ public void method(){ System.out.println("父类方法 "+this.getClass()); } public static void main(String[] args) { Father instence =new Son(); instence.method(); }}class Son extends Father{ public void method(){ System.out.println("子类方法 "+this.getClass()); }}结果:子类方法 class reviewInherit.Son
- 去掉子类的方法:会去执行父类的方法
class Father{ public void method(){ System.out.println("父类方法 "+this.getClass()); } public static void main(String[] args) { Father instence =new Son(); instence.method(); }}class Son extends Father{}结果: 父类方法 class reviewInherit.Son
- 动态绑定只是针对对象的方法,对于属性无效。因为属性不能被重写。
class Father{ private String name="父类"; public static void main(String[] args) { Father instance =new Son(); System.out.println(instance.name); }}class Son extends Father{ private String name="子类";}结果:父类
也可通过以下代码证明:
class Father{ private String name="父类";}class Son extends Father{ private String name="子类"; public static void main(String[] args) { Father instance =new Son(); System.out.println(instance.name); }}instance.name 处直接报错 'name' has private access in 'reviewInherit.Father'
4.要访问子类中特有的方法和属性,在编写代码时,则必须进行类型转换。
class Father{ private int count=10;}class Son extends Father{ private int count=100; public static void main(String[] args) { Son s =new Son(); Son instance=s; System.out.println(instance.count); }}结果 100
改为如下代码:
class Father{ private int count=10;}class Son extends Father{ private int count=100; public static void main(String[] args) { Son s =new Son(); Father instance=s; System.out.println(instance.count); }}instance.count 处直接报错: 'count' has private access in 'reviewInherit.Father'
对于 Class Father 和Class Son来说,Class Son是Class Father的子类,由于子类的变量并不会覆盖父类的变量,所以实际上在class B中是存在来两个count,在这分别记作Father.count
和Son.count
;
虽然在 Class Son中存在A.count
和B.count
,但是究竟输出那一个 count取决于该引用变量的声明时类型
重载和重写区别
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的参数列表,有兼容的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。重载对返回类型可以与父类的放回值类型一样,也可以是父类放回值类型的子类(其他情况报错)。
object中方法
Object 类属于
java.lang
包,此包下的所有类在使用时无需手动导入,系统会在程序编译期间自动导入
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z740UJZM-1632385272613)(D:\document\小林code\小林图片\Object类.png)]
toString()
在使用对象直接嗲用toString()
的时候,默认输出的是一个对象在堆内存上的地址值;如若要输出该对象的内容,则要覆写toString()
方法
在源码中,可以发现通过反射,获取到了当前对象的全限定类名和@十六进制哈希值字符串。这就是不覆写toString()
时直接打印输出的内容。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vNEVGe4Z-1632385272619)(D:\document\小林code\小林图片\原始toString方法.png)]
toHexString()
用于获取double类型的十六进制数字转换为字符串
public static String toHexString(double value);
equals()
-
只能判断引用类型
-
基本数据类型的比较用
==
(如: a == 3,b == 4, a == b,比较的是值是否相等) -
引用类型数据比较:调用
equals()
方法进行比较
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0lRpwhdZ-1632385272622)(D:\document\小林code\小林图片\重写equals方法.png)]
hashcode()
Object 对象的 hashCode() 方法会根据不同的对象生成不同的哈希值,默认情况下为了确保这个哈希值的唯一性,是通过将该对象的内部地址转换成一个整数来实现的。
通过计算,返回对象的hash值。相同的对象一定返回相同的hash值,但不同的对象不一定放回不同的hash值,也就是说,不同的对象可能产生相同hash值(hash冲突)。
/* a[]中包含的是对象的属性值*/public static int hashCode(Object a[]) { if (a == null) return 0; int result = 1; for (Object element : a) result = 31 * result + (element == null ? 0 : element.hashCode()); return result; }
通常我们在使用Set和Map集合时,不允许在集合有相同对象。这种机制就是使用hashcode()
的特性来实现的,但由于hash冲突的存在,就要求在重写了hashcode()
后,必须重写equals()
。
- 如果对象的hash值不同,就说明这对象不同,可直接加入到集合中。
- 如果对象hash值相同,还需要由
equals()
方法来进一步判断对象是否相同。
finalize()
当没有任何引用的指向一个对象时,则jvm就认为该对象就是一个垃圾对象,就会使用垃圾回机制来销毁该对象(释放该对象的堆内存空间),在对象销毁前会调用finaliz()
。程序员可以在finalize()方法中写入自己的业务逻辑代码(比如释放资源:关闭数据库连接,关闭文件)
clone()
浅拷贝
定义:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。即对象的浅拷贝会对“主”对象进行拷贝,但不会复制主对象里面的对象。”里面的对象“会在原来的对象和它的副本之间共享。
简而言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象。
-
使用拷贝构造函数实现浅拷贝:
-
使用
clone()
实现浅拷贝
深拷贝
定义:深拷贝是一个整个独立的对象拷贝,深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。
简而言之,深拷贝把要复制的对象所引用的对象都复制了一遍。
-
使用
clone()
实现 -
使用序列化实现深拷贝
clone()
为什么必须实现Cloneable
接口:
Cloneable
是标示接口与java.io.Serializable
类似,用于告知JVM
该对象实现clone
。并且super.clone()
可以返回一个复制。- 很多时候,会把protected的clone方法修改为public,暴露给其他人可以使用.详细信息,建议仔细阅读
JDK
的文档
多态
方法的重载又被称为:静态多态
方法的重写又被称为:动态多态
多态的细节:
- 一个对象的编译类型和运行类型可以不一致
- 编译类型在定义时已经确定,不能改变
- 运行类型是可以改变的
Animal animal = new Dog();animal= new Cat();
- 在我们编写源代码时(.java文件),只能调用编译时类型中具有的方法
- 代码运行过程中,调用的是运行时类型的方法。如果运行时类型没有该方法,就回到父类中寻找,和继承体系中的方法访问机制一样
向下转型
-
语法:子类类型 引用名 = (子类类型)父类引用
-
//编译类型和运行类型都是CatCat cat = (Cat)Animal;
-
要求父类引用必须指向的是当前目标类型的对象
instanceof
判断对象的运行时类型是否否为xx类型的类型或xx的子类型
Java动态绑定机制
- 当调用对象方法时,该方法和该对象的运行时类型绑定
- 调用对象的属性时,没有动态绑定机制,哪里声明,那里调用。
多态的引用
- 多态数组:数组的定义类型为父类类型,里面保存的实际类型是子类类型
- 多态参数:方法定义的形参是父类类型,实参允许为子类类型
断点调试
在断点调试过程中,程序处于运行状态。也就是说变量是运行时类型
调试快捷键
- 逐行指向代码 F8
- 跳入方法 F7
- 强制进入方法 Alt+Shift+F7
- 跳出方法 Shift+F8
- 跳到下一个断点 F9
当你在调试的过程中,依然可以断点,使用F9可以跳到下一个断点。但也会根据业务逻辑,来判断是否能到达下一个断点,如果不能,就会跳过。
代码块
-
相当于另一种形式的构造器(对构造器的一种补充机制),可以做初始化操作
-
当我们每创建一个对象时,都会先调用代码块中的内容。代码块中的调用优先于构造器
细节
- 静态代码块,作用就是对类对进行初始化,而且它随着类的加载而执行,并且只会执行一次。
- 如果是普通代码块,每创建一个对象实例,就会执行次。如果只调用静态成员,不会调用普通代码块
- 类声明时候会被加载:
- 创建对象实例
- 创建子类对象实例时,父类也会被加载
- 使用类的静态成员时(静态属性,静态方法)
创建对象时,在一个类中的调用顺序:
- 调用静态代码块和静态属性初始化(静态代码快和静态属性的调用优先级是一样的,如果有多个静态代码块和静态属性的初始化这按照定义顺序初始化)
- 调用普通代码块和普通属性(普通代码块和普通属性的)
- 构造方法
对象的加载流程
- 在方法区中加载类
- 属性隐式初始化(给默认值)
- 属性显示初始化(在创建属性时赋的值)
- 调用构造器初始化
当你调用一个构造器,为了更好理解,可以认为在构造器中隐藏了两段代码,例如:
public Person(){ super(); //本类的普通代码块和普通属性}
单例设计模式
饿汉式
public class SingerTon1 { public static void main(String[] args) { GirlFriend girlFriend1 = GirlFriend.getGirlFriend(); GirlFriend girlFriend2 = GirlFriend.getGirlFriend(); System.out.println(girlFriend1==girlFriend2); System.out.println(girlFriend1); }}class GirlFriend { private String name; // static b保证静态方法中返回 private static GirlFriend girlFriend = new GirlFriend("锅盖"); // private 外部不能创建对象 private GirlFriend(String name) { this.name = name; } public static GirlFriend getGirlFriend() { return girlFriend; } @Override public String toString() { return "GirlFriend{" + "name='" + name + '\'' + '}'; }}
饿汉式:如果类中有静态属性,被调用话。就会调用构造器,产生对象,容易造成资源浪费
懒汉式
public class SingerTon2 { public static void main(String[] args) { Dog dog= Dog.getInstance(); System.out.println(dog); }}class Dog{ private String name; private static Dog dog; private Dog(String name){ this.name=name; } public static Dog getInstance(){ if(dog==null){ dog=new Dog("元芳"); } return dog; } @Override public String toString() { return "Dog{" + "name='" + name + '\'' + '}'; }}
- 饿汉式和懒汉式的区别主要在于创建对象的时机不同,饿汉式是在类加载的时候创建,懒汉式是在使用时创建
- 饿汉式存在资源浪费的问题,懒汉式存在线程安全的问题
final
final
修饰变量,定义时必须赋值:有如下几种情况
- 定义时
- 代码块
- 构造器
如果final
修饰的是静态属性的话,只能在:
- 定义时
- 静态代码块
fina
类不能继承,但可以实例化
如果不是final
类,但有final
属性,属性可以被继承,不能被重写
如果一个类已经是final
方法,就没有必要将属性和方法修饰为final了
final
不能修饰构造方法
final
和static
往往搭配使用,效率更高,不会导致类的加载,底层编译器做了优化
包装类String
类都是final
修饰,不能被继承。
抽象类
- 抽象类不能被实例化
- 抽象不一定包含抽象方法(abstract修饰),即抽象类中可以没有抽象方法
- 一旦有抽象方法,就必须声明抽象类
- 抽象类本质还是类,抽象类可以有任意成员
- 如果一个类继承抽象类,则必须实现抽象类中的所有方法,除非声明自己为抽象类
- 抽象方法不能使用private,static,final修饰的,因为这些关键字是和方法重写相违背的
接口
接口中可以有三种类型的方法:
- 抽象方法
- default修饰的方法
- 静态方法
细节:
- 接口不能被实例化
- 接口中所有的方法都是public abstract方法,这两个关键字可以省略不写
- 一个普通类实现一个接口,必须实现接口中所有的方法
- 抽象类在实现接口时,可以不实现接口中的抽象方法
- 一个类中可以实现多个接口
- 接口中的属性,默认是public static final修饰的
- 接口中属性的访问方式:接口名.属性名
- 接口不能继承其他的类,但可以同时继承多个别的接口
- 接口的修饰符和类的修饰符,只能使用public和默认
- 一个类可以同时继承和实现类
- 接口中也有多态参数和多态数组
- 接口中存在多态传递现象
接口是对java单继承的一种补充
接口中的多态传递
public class C { public static void main(String[] args) { A a = new D(); B b = new D(); }}interface A{ void say();}interface B extends A{ void hi();}class D implements B{ @Override public void hi() { System.out.println("你好"); } @Override public void say() { System.out.println("憨憨"); }}
继承中的多态传递
public class E extends F{ public static void main(String[] args) { G g = new E(); }}class F extends G{}class G{}
类的五大成员
- 属性
- 方法
- 构造器
- 代码块
- 内部类
内部类
局部内部类
- 局部内部类本质仍然是一个类,定义在外部类中的局部位置(方法,代码块)
- 可以直接访问外部类的所有成员,包括私有成员
- 不能添加访问修饰符,但可以添加
final
(相当于一个局部变量) - 外部类访问局部内部类成员:先创建内部类对象,通过对象访问内部类成员
- 作用域:仅仅在定义局部内部类的方法或代码块中
- 外部其他内部类不能访问局部内部类
- 如果局部内部类和外部类成员重名时,默认遵守就近原则。如果想要在局部内部类中访问外部类重名的属性,可以使用
外部类.this.属性名
来访问。外部类.this
就表示调用该方法的对象
匿名内部类
匿名内部类的本质还是一个类,定义在外部类的局部位置。其实也相当于一个对象。
接口的匿名内部类:
public class AnonInter { public static void main(String[] args) { Inner inner = new Inner() { @Override public void say() { System.out.println("hello"); } }; System.out.println(inner.getClass()); //class anonymous.AnonInter$1 }}interface Inner{ void say();}
编译时类型:Inner
运行时类型:AnonInter$1
其实在JDK的底层是会创建一个类(外部类$1
),该类隐式的实现了Inner
接口。创建该类的实例对象,将对象的地址放回。我们可以通过对象名.getClass()
来获取匿名内部类的运行时类型
匿名内部类只能使用一次
类的匿名内部类
public class AnonClass { public static void main(String[] args) { Father father = new Father(){ @Override public void say() { System.out.println("hello"); } }; System.out.println(father.getClass()); //class anonymous.AnonClass$1 }}class Father{ public void say(){ System.out.println("你好"); }}
当我们在new Father()
后加了括号,就不再是创建一个对象了,而是一个匿名内部类,她在底层隐式继承了Father
类,所以可重写父类的方法
编译时类型:Father
运行时类型:AnonClass$1
如果使用匿名内部类时,有参数传入的话,会传给Fathe构造器
使用匿名内部类
public class Anon { public static void main(String[] args) { Base base=new Base(){ @Override public void say() { System.out.println("hello"); } }; base.say(); //hello new Base(){ @Override public void say() { System.out.println("Hello Word"); } }.say(); //Hello Word }}class Base{ public void say(){ System.out.println("你好"); }}
使用细节:
- 可以访问外部类的所有成员,包括私有成员
- 不能添加访问修饰符,因为匿名内部类相当于一个局部成员
- 作用域:仅仅在定义它的方法会代码中
- 当匿名内部类和外部类有同名成员时,遵循就近原则
- 外部其他类不能访问匿名内部类中的成员
成员内部类
-
可以直接访问外部类的所有成员
-
可以使用任意修饰符修饰,因为成员内部类就相当于一个成员属性
-
作用域:外部类中都可以使用,地位相当于成员属性
-
成员内部类访问外部类成员的方式:直接访问
-
外部类访问成员内部类成员的方式:先创建成员内部类对象,通过对象访问成员
-
外部其他类访问成员内部类的方式:
public class Test { public static void main(String[] args) { Out out = new Out(); Out.Inner inner = out.new Inner(); inner.say(); Out.Inner innerInstance = out.getInnerInstance(); innerInstance.say(); }}class Out{ public class Inner{ public void say(){ System.out.println("hello"); } } public Inner getInnerInstance(){ return new Inner(); }}
- 如果成员内部类和外部类有重名的成员,遵循就近原则。想要在成员内部类中访问外部类的成员,就需要使用
外部类类名.this.属性
- 如果成员内部类和外部类有重名的成员,遵循就近原则。想要在成员内部类中访问外部类的成员,就需要使用
静态内部类
-
使用static修饰,放在外部类的成员位置
-
可以直接访问外部类的所有静态成员,包括私有成员
-
可以添加任意访问修饰符
-
作用域:整个外部类类体
-
外部其他类访问静态内部类
public class Test { public static void main(String[] args) { //外部其他类访问静态内部类成员 //1. 使用外部类名创建对象 Out.Inner inner = new Out.Inner(); //2. 返回对象实例 Out out = new Out(); //使用普通方法 Out.Inner innerInstance = out.getInnerInstance(); //使用静态方法 Out.Inner instance = out.getInstance(); }}class Out{ static class Inner{ public void say(){ System.out.println("hello"); } } public Inner getInnerInstance(){ return new Inner(); } public static Inner getInstance(){ return new Inner(); }}
-
如果静态内部类和外部类有重名的成员,遵循就近原则。想要在成员内部类中访问外部类的成员,就需要使用
外部类类名.属性
注解
元注解
@Target
修饰注解的注解,元注解。表明注解可以修饰那些元素
@Retention
可以指定保留的策略
@Documented
使用javadoc工具提取成文件,及生成文档时,可以看到该注解
@interface
表示一个注解类
@Override
表明方法被重写
@Target(ElementType.METHOD)@Retention(RetentionPolicy.SOURCE)public @interface Override {}
@Deprecated
表明元素已经过时
@Documented@Retention(RetentionPolicy.RUNTIME)@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})public @interface Deprecated {}
@SuppressWarnings
抑制警告
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})@Retention(RetentionPolicy.SOURCE)public @interface SuppressWarnings { /** * The set of warnings that are to be suppressed by the compiler in the * annotated element. Duplicate names are permitted. The second and * successive occurrences of a name are ignored. The presence of * unrecognized warning names is <i>not</i> an error: Compilers must * ignore any warning names they do not recognize. They are, however, * free to emit a warning if an annotation contains an unrecognized * warning name. * * <p> The string {@code "unchecked"} is used to suppress * unchecked warnings. Compiler vendors should document the * additional warning names they support in conjunction with this * annotation type. They are encouraged to cooperate to ensure * that the same names work across multiple compilers. * @return the set of warnings to be suppressed */ String[] value();}
异常
try–catch快捷键(Ctrl+Alt+T)
异常可以分为两大类:运行时异常和编译时异常
运行时异常
编译器检查不出来。一般是指编程时的逻辑错误,是程序员应该避免的异常。对于运行时异常,可以不做处理,因为这类异常很普遍,若全处理可能会对程序的可读性和运行效率产生影响
- NullPointerException
- ArrayIndexOutOfBoundException
- ClassCastException(出现在引用类型向上转型时)
- NumberFormatException
- ArithmeticException
编译时异常
编译时要求必须处理的异常
try-catch-finally
- try:try中发的是程序员写的可能产生异常的代码
- catch:当异常发送时,系统将异常封装成Exception对象,得到异常对象后可以程序员可以自己处理。当没有异常产生时,catch中的代码不会执行
- finally:不管try块中是否产生了异常,都会执行finally中的代码,一般用于资源的释放
throws
- 将异常抛出给方法的调用者,JVM顶级是顶级调用者。
- 如果没有显示的处理异常,默认就是throws
- 当异常抛给JVM,就会打印异常信息,并中断程序执行
- 子类重写父类的方法,抛出的异常与父类的相同,或是父类抛出异常的子类
- 在使用throws过程中,就没有必要使用try-catch。
- 如果一个方法中有运行时异常抛出,那么该方法的调用者也必须显示的抛出该异常或try-catch,不做处理就会报错。
- 如果一个方法中抛出的是编译时异常,可以不显示的处理,因为有默认的处理机制。
使用细节
- 在try块中,如果异常发生了,则异常后面的代码就不会执行,直接进入到catch块中
- 如果异常没有发生就会顺序执行try块中的代码,不会进入到catch中
- 当try块中可能有多个异常时,可以使用多个catch块来分别捕获。要求子类异常在前,父类异常在后。如果发生异常只会匹配一个catch块
- 可以进行try-finally配合使用,这种用法相当于没有捕获异常,因此程序会直接崩掉。
- 如果catch块中有return语句。程序会先执行catch中的return语句,但不直接返回,而是把catch块中return的结果保存下来。因为finally块是一定要执行的。
- 如果finally块中也有return语句,就会直接返回finally块中的return,方法执行完毕。
- 如果finally中没有return语句,就会返回catch块中保留的return结果
使用异常实现输入数字
public class InputInteger { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); int num; while(true){ System.out.println("请输入一个整数"); try { num = Integer.parseInt(scanner.next()); break; } catch (NumberFormatException e) { System.out.println("你的输入有误"); } } System.out.println("你输入的数字为:"+ num); }}
自定义异常
创建一个类继承RuntimeException类即可。我们一般都将其定义为一个运行时异常,可以采用默认的处理机制,使用起来比较方便。
意义 | 位置 | 后跟 | |
---|---|---|---|
throw | 手动生成异常对象的关键字 | 方法体中 | 异常对象 |
throws | 异常处理的一种方式 | 方法声明中 | 异常类型 |
包装类
继承图
有了包装类对象,就可以使用类的方法
自动装箱和自动拆箱
Integer integer = 20; //自动装箱 底层调用Integer.Valueof(20)int n = integer; //自动拆箱 底层调用Integer.intValue(integer)
题目练习
Object obj = true? new Integer(1): new Double(1);//输出 1.0//因为三元运行符是一个整体,会提升优先级
if(true){ System.out.println(new Integer(1));}else{ System.out.println(new Double(1));}//输出1
缓冲池数值比较问题
只要有基本数据类型比较,就是是值的比较
Integer i1 = 128;int i2 =128;System.out.println(i1==i2); //ture
String
- String对象用于保存字符串,也就是一组字符串序列
- 字符串的字符使用的是Unicode编码,一个字符(不区分字母还是汉字)占两个字节
- String类有很多构造器,构造器的重载
- String类实现了Serializable接口,可以实现串行化,在网络中传输
- String类实现了Comparable接口,可进行字符串的比较
- String类被final修饰,不能被其他类继承
- String类中属性:
private final char value[]
用于存放字符串内容,value[]
被final修饰,value的指向就不可以修改了,但value[]
中内容是可以修改的。
创建String对象两种方式的区别
方式一
String str = "hsp";
先看常量池中是否有hsp
,如果有,主栈中的str引用变量就直接指向常量池中hsp
,如果没有就先创建,再指向
方式二
Sting str = new Integer("hsp");
现在堆中创建一个内存空间,里面维护value
属性,指向常量池中的hsp
。如果在常量池中没有hsp
,先创建,再指向。主栈中的str指向堆中创建的内存空间。
intern()
当调用intern()
方法时,如果包含一个等于此String对象的字符串(用equal()
确定),这返回池中字符串的地址。否则将String对象的字符串添加到常量池中,并返回常量池中引用。最终返回的都是常量池中的地址
String-->char[]
String str = "zzh";char[] chars = str.toCharArray();
String的compareTo()
public int compareTo(String anotherString) { int len1 = value.length; int len2 = anotherString.value.length; int lim = Math.min(len1, len2); char v1[] = value; char v2[] = anotherString.value; int k = 0; while (k < lim) { char c1 = v1[k]; char c2 = v2[k]; if (c1 != c2) { return c1 - c2; } k++; } return len1 - len2; }
练习题
String str = "a";str = "b"; //常量池中创建了两个对象:改变了str的指向
String a = "hello" + "abc";//创建一个对象:编译器进行了优化//等价于 String a = "helloabc";
String a = "hello";String b ="abc";String c = a + b;/* 1.先创建一个 StringBurder sb = new StringBurder(); 空对象 2.执行 sb.append("hello"); 3.sb.append("abc"); 4.String c = sb.toString(); 最后其实c指向的是堆中的(String)对象 value[],value[] ->池中"helloabc"*/
常量相加,看的是池。变量相加,看的是堆。
class Test{ String str = new String("hsp"); final char[] ch = {'j','a','v','a'}; public void change(String str,char[] ch){ str = "java"; ch[0] = 'h'; }}class Test3{ public static void main(String[] args) { Test test = new Test(); test.change(test.str, test.ch); System.out.print(test.str + " and "); System.out.println(test.ch); }}//只要有一个方法在栈中就会创建一个帧,当该方法用完,就会出栈//最后输出hsp and hava
内存图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZF7mmOrt-1632385272625)(D:\document\小林code\小林图片\字符串内出图.png)]
fromat()
- %s,%d,%.2f,%c 称为占位符,这些占位符有后面的变量替换
- %s 用字符串替换
- %d 用整数替换
- %.2f 用小数来替换,只会保留两位小数(四舍五入)
- %c 用char类型替换
StringBuffer
- StringBuffer的直接父类是AbstractStringBuilder
- StringBuffer实现了Serializable,既StringBuffer的对象可以串行话
- 在父类AbstractStringBuilder有一个属性
char[]
value,不是final,该value数组存放字符串内容 - StringBuffer被final修饰,不能被继承
String和StringBuffer的区别
String保存的是字符串常量,里面的值不能修改,每次内容的更新,其实是引用变量指向一个新的引用变量,效率较低
StringBuffer保存的字符串的变量,里面的值可以更改,每次内容的更新,就是value[]
数组内容的改变,效率较高
String-->
StringBuffer
String str = "hello";//使用构造器 放回的是一个StringBuffer对象,对str本身没有影响StringBuffer sb = new StringBuffer(str);//使用append()StringBuffer sb1 = new StringBuffer();sb1 = sb1.append(str);
StringBuffer-->
String
//使用StringBuffer提供的toString()StringBuffer sb2 = new StringBuffer();String str = sb2.toString();//使用构造器String str1 = new String(s)
练习
String str = null;StringBuilder sb = new StringBuilder();sb.append(str);System.out.println(sb.length()); //sb为null,输出4StringBuffer sb1 = new StringBuffer(str);System.out.println(sb1.length());//抛出空指针异常
StringBuffer中有insert()
,经常用于插入字符串
StringBuilder
StringBuilder是StringBuffer的一个建议替换,存在线程安全问题,常用于单线程,速度比StringBuufer快
- StringBuilder 继承 AbstractStringBuffer类
- 实现了Serializable,可以实现串行化,在网络中传输
- StringBuilder是final类,不能被继承
- StringBuilder中存放字符序列的,依然是 AbstractStringBuffer类中
char[]
value,因此字符序列在堆 - StringBuilder 的方法没有互斥处理,即没有synchronized关键字修饰,因此在单线程情况下使用
String,StringBuffer和StringBuilder的比较
- StringBuffer 和 StringBuilder 都代表可变字符序列,而且方法也一样
- String:不可变字符序列,效率低,但复用率高
- StringBuffer:可变字符序列,效率较高(增删),线程安全
- StringBuilder:可变字符序列,效率最高,线程不安全
结论:如果我们要对字符串进行大量修改,尽量不要使用String
Math
public static void main(String[] args) { rand(1,10);}//生成[a,b]的随机数public static void rand(int a, int b){ for (int i = 0; i < 10; i++) { int ran = (int)(Math.random()*(b-a+1)+a); System.out.println(ran); }}
Arrays方法
-
toString()
返回数组的字符串形式Arrays.toString(arr)
-
sort排序(自然排序和定制排序)
Arrays.sort(arr)Arrays.sort(arr, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o1 - o2; } });
-
binarySearch 通过二分查找索引法进行查找,要求必须排序。
- 如果存在就返会数字所在索引
- 如果不存在就返回数字应该在数组中的索引+1,取负数
return -(low + 1)
-
copyOf 数组的复制
int[] newArr = Arrays.copyOf(arr,arr.length);int[] newArr = Arrays.copyOf(arr,arr.length + 1);//如果拷贝的长度 > arr.length 就在新数组中的后面,增加nullint[] newArr = Arrays.copyOf(arr,arr.length - 1);//如果拷贝的长度 < 0 就会抛出异常NegativeArraysSizeException
-
fill()
Arrays.fill(num,99);//使用99替换num数组的元素
-
equals()
比较数组元素内容是否一致Arrays.equals(arr1,arr2)
-
asList()
将数组转为List集合List list = Arrays.asList(arr);// 运行时类型java.util.Arrays#ArraysList /Arrays类中的一个内部类
System类
-
exit()
: 退出当前程序。参数一般为0,表示正常退出 -
arryaycopy()
:复制数组元素,比较适合底层调用。一般使用Arrays.copyOf() 来完成数组调用/*src – the source array.srcPos – starting position in the source array.dest – the destination array.destPos – starting position in the destination data.length – the number of array elements to be copied*/public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
-
currentTimeMillens()
:放回当前时间距离1970-1-1的毫秒数 -
gc()
: 运行垃圾回收机制
大数据处理
BigInteger类
BigInteger bigInteger = new BigInteger("29999999999999999999");BigInteger bigInteger1 = new BigInteger("99999");//加法 :参数类型和放回值都是BigIntegerBigInteger add = bigInteger.add(bigInteger1);//减法BigInteger subtract = bigInteger.subtract(bigInteger1);//乘法BigInteger multiply = bigInteger.multiply(bigInteger1);//除法BigInteger divide = bigInteger.divide(bigInteger1);
BigDecimal类
BigDecimal bigDecimal = new BigDecimal("19.99999999999999999297");BigDecimal bigDecimal1 = new BigDecimal("17");System.out.println(bigDecimal.divide(bigDecimal1));//Non-terminating decimal expansion; 这里肯会抛出异常//处理方法System.out.println(bigDecimal.divide(bigDecimal1, RoundingMode.CEILING));//保留小数位数和分子的小数位数相同
- BigInteger 适合保存比较大的整型
- BigDecimal 适合进度更高的浮点数
时间函数
Data类
public Date(long date) { fastTime = date;}public Date() { this(System.currentTimeMillis());}//获取当前系统时间 Date date = new Date(); //得到当前时间的毫秒数 System.out.println(date.getTime()); //设置日期输出的格式 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy年MM月dd日 hh:mm:ss"); System.out.println(simpleDateFormat.format(date)); //2021年09月07日 10:22:12 //字符串转为日期格式 Date parse = simpleDateFormat.parse("2021年09月07日 10:22:12"); System.out.println(simpleDateFormat.format(parse));
Calendar类
-
Caldedar类是一个抽象类,并且构造器是是私有的
-
可以通过
getInstance()
来获取对象实例 -
提供的大量的字段和方法
Calendar instance = Calendar.getInstance();System.out.println(instance);
-
Cladedar类在返回月时,是按照0开始编号
-
没有提供日期格式的方法,可以自由灵活的定义日期格式
第三代日期
class TestLocalDate{ public static void main(String[] args) { LocalDateTime localDateTime = LocalDateTime.now(); System.out.println(localDateTime); System.out.println(localDateTime.getYear()); System.out.println(localDateTime.getMonth()); System.out.println(localDateTime.getMonthValue()); System.out.println(localDateTime.getDayOfMonth()); System.out.println(localDateTime.getHour()); System.out.println(localDateTime.getMinute()); System.out.println(localDateTime.getSecond()); //只包含年月日 LocalDate localDate = LocalDate.now(); System.out.println(localDate.getYear()); //只包含时分秒 LocalTime localTime = LocalTime.now(); System.out.println(localTime.getHour()); //日期格式化 DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 hh:mm:ss"); System.out.println(dateTimeFormatter.format(localDateTime)); }}class TestInstant{ public static void main(String[] args) { //通过静态方法 now() 获取当前时间戳对象 Instant now = Instant.now(); System.out.println(now); //通过from 把Instant转为Date Date date = Date.from(now); //通过date的toInstant() Date转为Instant Instant instant = date.toInstant(); }}
集合
集合的优点:
- 可以动态保存任意类型的多个对象,使用比较方便
- 提供了一系列方便操作对象的方法:add,remove,set,get
- 代码简洁
集合主要分为:单例集合,双列集合
Collection(单列集合)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SllO7j4s-1632385272627)(D:\document\小林code\小林图片\Collection继承图.png)]
Iterator接口
-
Iterator对象称为迭代器,主要用于遍历Collection集合中元素
-
Collection接口继承了Iterator接口,所以每个实现了Collection的集合,都有一个
iterator()
方法,该方法放回一个Iterator对象,即放回一个迭代器 -
Iterator 仅用于遍历集合,Iterator本身不存放对象
-
hasnext()
判断该是否有下一个元素 -
next()
:指针移向下一个元素,并放回该元素。 -
使用
Iterator
遍历集合Iterator iterator = list.iterator();while (iterator.hasNext()) { Object next = iterator.next();}/* 当退出while时,iterator迭代器,指向最后一个元素,如果想要再次遍历,需要重置*/iterator = list.iterator();
-
使用增强for遍历集合(可代替Iterator,简化版的迭代器)
for (Object o : list) { System.out.println(o);}
List
-
List 集合中的元素有顺序(即添加顺序和输出顺序相同),且可重符
-
List集合中每个元素都有器对应的顺序索引,及支持索引
System.out.println(list.get(2));
-
List 集合还可以使用普通for进行遍历
for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i));}
ArrayList
- ArrayList 可以存放空,且可以存放多个
- ArrayList 底层使用数组实现的
- ArrayList 基本等同于 Vector,ArrayList的线程不安全(执行效率高),在多线程情况下,不建议使用ArrayList
源码分析
-
ArrayList中维护的是一个
Object[] elementData
transient Object[] elementData;
-
创建ArrayList对象是,使用无参构造器,初始容量为0,第一次添加时扩容为10,如以后还需要扩容,则扩容为原来的1.5倍
public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
-
使用有参构造器,可以指定ArrayList的初始容量,以后需要扩容时,扩容为原来的1.5倍
public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } }
-
扩容机制
public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length > 0) grow(minCapacity); } private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); if (newCapacity - minCapacity < 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); }
Vector
public class Vector<E>extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable
-
底层是一个
Object
对象数组protected Object[] elementData;
-
Vector是线程安全的,Vector类的操作方法都带有
synchronized
关键字
LinkedList
-
底层实现了双向链表和双端队列
-
可以添加任何元素,包括null
-
第一个节点的
prev = null
,最后一个节点的next = null
-
线程不安全
-
常用方法:
LinkedList linkedList = new LinkedList();linkedList.add(999);linkedList.remove();linkedList.get(1);linkedList.set(0,999);
Set
- 无序(添加和取出的顺序不一致),没有索引,所以不能使用普通for遍历
- 不允许有重复元素,所以最多只包含一个null
- 输出的顺序虽然不是添加的顺序,但输出的顺序是固定的
HashSet
-
HashSet底层其实是HashMap
public HashSet() { map = new HashMap<>(); }
源码分析
-
在HashSet的底层有一个HashMap对象
private transient HashMap<E,Object> map;
-
成员变量,
private static final Object PRESENT = new Object();
,该成员变量的作用就是为了HashSet
可以使用HashMap
,而占位用的 -
/*The table, initialized on first use, and resized as necessary. When allocated, length is always a power of two. (We also tolerate length zero in some operations to allow bootstrapping mechanics that are currently not needed.)表,在第一次使用时初始化,并根据需要调整大小。 分配时,长度始终是 2 的幂。 (我们还在某些操作中容忍长度为零,以允许当前不需要的引导机制。)*/transient Node<K,V>[] table;
putVal()
源码分析
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //定义了辅助变量 //table 就是 transient Node<K,V>[] table; //if表示如果当前table 是 null 或容量为0.就进行第一次扩容,容量大小为16 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; /*根据hash值类确定元素应存放在table的那个位置,并把这个位置的地址赋值给p,判断p是否为空 如果为空,就表明该位置还没有存放元素,就创建一个节点,这里把hash值也传了进去,用于下一次存 放元素时,进行比较是否相同 */ if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); //当table表中索引位置已经有元素存在,就进入到else块中 else { Node<K,V> e; K k; /* 该对象的hash值和该索引位置的第一个对象的hash值相同&& (引用变量key相同 || 内容相同) */ if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { //这里会让要添加的元素和当前索引下的元素依次进行比较 for (int binCount = 0; ; ++binCount) { //如果没有相同的元素,将元素添加进去,退出循环 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); /* 再把元素添加到链表后,就立即判断是否达到了8个节点,如果达到就调用 treeifyBin(tab, hash);将链表转换为一个红黑树。 在treeifyBin(tab, hash)中,还要进行判断: if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); 即table的长度小于64时,先要对table进行扩容 */ if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //如果发现有相同的元素,直接退出循环 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; //移向下一个元素,继续进行比较 p = e; } } //存在相同的key,ji if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }//最后返回的结果如果为null,就代表添加元素成功,否则,添加失败
-
resize()
源码分析final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; //临界值:表示当数组的内容个数达到该值,就要扩容 int newCap, newThr = 0; if (oldCap > 0) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // zero initial threshold signifies using defaults //static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 默认容量大小16 newCap = DEFAULT_INITIAL_CAPACITY; //static final float DEFAULT_LOAD_FACTOR = 0.75f; 装载因子 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
-
HashSet底层第一次添加元素时,table数组扩容为16,临界值为16*0.75=12
-
如果数组中的元素个数(不管该元素加在哪里,只要加入到HashSet中就算)到达了临界值,就会扩容为原来的2倍
-
当一条链表的个数达到8,并且table的容量>=64时,才会将链表转换为红黑树。如果一条链表的个数达到了8,但table容量<64,会对table进行扩容,扩容还是按照扩容到原来的二倍。
LinkedHashSet
-
LinkedHashSet是HashSet的一个子类
-
LinkedHashSet底层是一个LinkedHashMap,维护一个数组(table) + 双向链表
-
LinkedHashSet根据元素的hash值来决定元素存储的位置,同时使用链表维护元素的顺序,这使元素看起来是以插入顺序保存的
-
LinkedHashSet不允许有重复元素
-
第一次添加元素时,直接将数组
table
的容量扩容到16,存放的节点类型是:LinkedHash’Map$Entry -
数组
table
是HashMap$Node[]
,存放的元素是 LinkedHashMap$Entry类型//Entry 是在LinkedHashMap中的一个静态内部类static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } }
Map
-
Map用于保存具有映射关系的数据:Key–Value(双列元素)
-
Map中的Key和Value可以是任何引用类型的数据,会封装到HashMap$Node 对象中
transient Node<K,V>[] table;
-
Map中的可以不允许重复,原因和HashSet一样。当有相同的key是,就会替换原来的Value
-
Map中Value是可以重复的
-
Map中的Key可以为null,Value也可以为null,当Key为空只能有一个,Value为空可以有多个
-
常用String类做Key值
-
我们总能通过Key找到对应的Value
-
HashMap没有首先线程同步,所以线程是不安全的
Key—Value的存放机制
-
k-v首先是存放在一个Node中
//HashMap中的静态内部类 static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; }
-
通过
newNod()
返回以Node对象Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) { return new Node<>(hash, key, value, next); }
-
放入table数组中
tab[i] = newNode(hash, key, value, null);
-
k-v 为了方便程序员的遍历,还会创建entrySet的Set集合,该集合存放的元素类型 Entry
//HashMap中的一个静态内部类final class EntrySet extends AbstractSet<Map.Entry<K,V>>//Map接口中内部接口interface Entry<K,V>;//HashMap中成员transient Set<Map.Entry<K,V>> entrySet;//HashMap中方法public Set<Map.Entry<K,V>> entrySet() { Set<Map.Entry<K,V>> es; return (es = entrySet) == null ? (entrySet = new EntrySet()) : es; }
-
entrySet中定义类型是Map.Entry,但实际存放的还是 HashMap$Node
static class Node<K,V> implements Map.Entry<K,V>
-
当把 HashMap$Node对象存放到entrySet中就会方便我们遍历,因为Map.Entry提供了重要的方法
K getKey();V getValue();
-
需要注意的是:在entrySet只是指向了Node,并没有真正的存放数据
-
我们还可以得到Key和Value的Set集合
Set set = hashMap.keySet(); //运行时类型:class java.util.HashMap$KeySetCollection values = hashMap.values(); //运行时类型:class java.util.HashMap$Values
final class KeySet extends AbstractSet<K>final class Values extends AbstractCollection<V>
Map中的常用方法
HashMap hashMap = new HashMap();hashMap.put("zzh","guogai");hashMap.get("zzh");hashMap.size();hashMap.isEmpty();hashMap.containsKey("zzh");hashMap.containsValue("guogai");hashMap.remove("zzh");hashMap.clear();
Map的遍历方式
- 使用EntrySet(HashMap的内部类) 获取k-v
Set entrySet = hashMap.entrySet();System.out.println(entrySet.getClass()); //class java.util.HashMap$EntrySetfor (Object entry : entrySet) { Map.Entry m = (Map.Entry) entry; System.out.println(m.getKey()+" - "+m.getValue());}
- 使用keySet,获取k-v
Set keySet = hashMap.keySet();for (Object key :keySet ) { System.out.println(key + " - " + hashMap.get(key));}Iterator iterator = keySet.iterator();while (iterator.hasNext()) { Object next = iterator.next(); System.out.println(next + " - "+ hashMap.get(next));}
只获取value值
- 使用values获取
Collection values = hashMap.values();for (Object value : values) { System.out.println(value);}Iterator iterator1 = values.iterator();while (iterator1.hasNext()) { Object next = iterator1.next(); System.out.println(next);}
HashMap
- hashMap底层维护一个Node类型的数组table,默认为null
- 当创建对象时,将加载因子(loadfactor)初始化为0.75
- 当添加key–value时,通过key的hash值来确定在table中的索引。然后判断该索引处是否有元素,如果没有,则直接添加进去。如果索引处有元素,就判断key值是否重复,如果重复就替换掉原来的value值,没有重复就添加元素
- 第一次添加需要扩容到16,临界值为12
- 当添加的元素个数达到临界值,在需要扩容时,就扩容为原来的两倍
- 在java8中,如果一条链表的元素个数超过8个,并且table的容量>=64,就会将链表转为红黑树
Hashtable
-
存放的元素时键值对:k–v
-
Hashtable的键值不能为null,否则或抛出空指针异常
-
Hashtable的使用方法,基本和HashMap一样
-
Hashtable是线程安全的(synchronized),HashMap是线程不安全的
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable
-
底层有一个数组
private transient Entry<?,?>[] table;
初始容量:11,加载因子:0.75,临界值:8 -
扩容机制
int newCapacity = (oldCapacity << 1) + 1;
Properties
-
Properties 继承了 Hashtable,具有Hashtable的特点
-
Properties 主要用于读取配置文件
-
常用方法
Properties properties = new Properties();properties.put("1","zzh");properties.put("2","锅盖");System.out.println(properties.get("1"));properties.remove("1");properties.put("2","guogai");System.out.println(properties);
TreeSet
-
底层是TreeMap
-
构造器可传入一个比较器对象,赋值给TreeSet底层TreeMap的属性
this.comparator
-
底层比较的执行机制
Comparator<? super K> cpr = comparator; if (cpr != null) { do { parent = t; cmp = cpr.compare(key, t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else return t.setValue(value); } while (t != null); }
-
从底层的代码可以知道:当
compareTo()
放回0时,就认为这两个数据相同,就不会加入到TreeSet中了
TreeMap
-
底层维护一个
private transient Entry<K,V> root;
-
采用红黑树的数据结构
static final class Entry<K,V> implements Map.Entry<K,V> { K key; V value; Entry<K,V> left; Entry<K,V> right; Entry<K,V> parent; boolean color = BLACK; /** * Make a new cell with given key, value, and parent, and with * {@code null} child links, and BLACK color. */ Entry(K key, V value, Entry<K,V> parent) { this.key = key; this.value = value; this.parent = parent; }
-
添加元素机制
public V put(K key, V value) { Entry<K,V> t = root; //第一次添加元素,直接将元素挂在root上去 if (t == null) { //这里调compare方法是为了检查第一次传经来的值是否为null,如果为 compare(key, key); // type (and possibly null) check root = new Entry<>(key, value, null); size = 1; modCount++; return null; } int cmp; Entry<K,V> parent; // split comparator and comparable paths Comparator<? super K> cpr = comparator; //通过循环来确定要加入元素在树中的位置 if (cpr != null) { do { parent = t; cmp = cpr.compare(key, t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else //如果通过compareTO()比较有相同的key值,会发生替换 return t.setValue(value); } while (t != null); } else { if (key == null) throw new NullPointerException(); @SuppressWarnings("unchecked") Comparable<? super K> k = (Comparable<? super K>) key; do { parent = t; cmp = k.compareTo(t.key); if (cmp < 0) t = t.left; else if (cmp > 0) t = t.right; else return t.setValue(value); } while (t != null); } Entry<K,V> e = new Entry<>(key, value, parent); if (cmp < 0) parent.left = e; else parent.right = e; fixAfterInsertion(e); size++; modCount++; return null; }
-
如果通过compareTO()比较有相同的key值,会发生替换
如何选择集合实现类
- 先判断存储的类型(单列还是双列)
- 单列:Collection接口
- 允许重复:List
- 增删多:LinkedList(底层维护一个双向链表)
- 改查多:ArrayList(底层维护一个数组)
- 不允许重复:Set
- 无序:HashSet(底层是HashMap,维护一个哈希表:数组+链表+红黑树)
- 排序:TreeSet()
- 插入顺序和取出顺序一致:LinkedHashSet(维护数组+双向链表)
- 允许重复:List
- 双列:Map
- 键无序:HashMap
- 键排序:TreeMap
- 键插入顺序和取出顺序一致:LinkedHashMap
- 读取文件:Properties
Collections工具类
ArrayList arrayList = new ArrayList(); arrayList.add("zzh"); arrayList.add("erha"); arrayList.add("guogai"); arrayList.add("yuanfang"); //反转list Collections.reverse(arrayList); //随机排序 Collections.shuffle(arrayList); //按照自然排序 Collections.sort(arrayList); //指定排序规则排序 Collections.sort(arrayList, new Comparator<Object>() { @Override public int compare(Object o1, Object o2) { String str1 = (String)o1; String str2 = (String)o2; return str1.length()-str2.length(); } }); //交换list集合中的元素 Collections.swap(arrayList,0,2); //按照自然排序,返回给定集合中的最大元素 Collections.max(arrayList); //按照指定排序,返回集合中的最大元素 Collections.max(arrayList, new Comparator<Object>() { @Override public int compare(Object o1, Object o2) { String str1 = (String)o1; String str2 = (String)o2; return str1.length()-str2.length(); } }); //返回集合中指定元素的次数 Collections.frequency(arrayList,"zzh"); /* void copy(List dest,List src):将src中的内容复制到dest中 为了完成完整拷贝,需要个dest赋值,和arrayList的大小一样 public static <T> void copy(List<? super T> dest, List<? extends T> src) { int srcSize = src.size(); if (srcSize > dest.size()) throw new IndexOutOfBoundsException("Source does not fit in dest"); */ ArrayList dest = new ArrayList(); for (int i = 0; i < arrayList.size(); i++) { dest.add(""); } Collections.copy(dest,arrayList); //替换集合中的元素 Collections.replaceAll(arrayList,"zzh","zzhG");
泛型
泛型的好处
- 编译时,检查添加元素的类型,提高了安全性
- 减少了类型转换的次数,提高效率
- 不会提示编译警告
对泛型的理解
-
泛型是可以表示数据类型的一种数据类型
-
泛型又称参数化类型,解决数据类型安全性的问题
-
在类声明或实例化时只要指定好具体的类型即可
-
Java泛型可以保证在编译时没有发出警告,在运行时就不会产生ClassCastException异常,同时,代码更加简洁,健壮
-
泛型的作用:可以在类声明时,通过泛型表示类中某个属性的类型,或者方法的返回值类型,或者参数类型
class Person<E>{ private E name; public Person(E name) { this.name = name; } public E getName(){ return name; } public void setName(E name){ this.name = name; }}
-
泛型的使用
HashSet<Student> students = new HashSet<>(); students.add(new Student("zzh",18)); students.add(new Student("guogai",19)); students.add(new Student("yuanfang",20)); Iterator<Student> iterator = students.iterator(); while (iterator.hasNext()) { Student student = iterator.next(); System.out.println(student); } for (Student student :students) { System.out.println(student); } //在我们定义HashMap对象时,就已经确定了泛型的类型,之后Iterator中泛型类型也是已经确定的 HashMap<String, Student> hashMap = new HashMap<String, Student>(); hashMap.put("zzh",new Student("zzh",18)); hashMap.put("guogai",new Student("guogai",19)); hashMap.put("yuanfang",new Student("yuanfang",20)); Set<Map.Entry<String, Student>> entries = hashMap.entrySet(); Iterator<Map.Entry<String, Student>> entryIterator = entries.iterator(); while (entryIterator.hasNext()) { Map.Entry<String, Student> studentEntry = entryIterator.next(); System.out.println(studentEntry.getKey()+ " - "+studentEntry.getValue()); }
使用细节
-
泛型(T,E,R,M)只能是引用类型,不能为基本数据类型
-
在给泛型指定具体类型后,可以传入该类型和该类型的子类型
-
泛型的写法
HashSet<Student> students = new HashSet<Student>();//也可以简写,编译器会进行类型推断HashSet<Student> students = new HashSet<>();//如果没有给泛型指定具体的类型,默认是ObjectHashSet students = new HashSet();HashSet<Object> students = new HashSet<>();
自定义泛型
自定义泛型类
- 定义泛型时,可以定义多个泛型
- 普通成员(属性,方法)可以使用泛型
- 使用泛型的数组,不能初始化(因为在创建数组时,不能确定泛型的类型,就无法在内存中开辟空间)
- 静态方法中不能使用泛型,静态属性不能使用泛型(因为静态是和类相关的,在类加载时,对象还没有创建,所以,如果静态方法和静态属性使用了泛型,
JVM
就无法完成初始化) - **泛型的类型是在创建对象时确定的(**因为创建对象时,需要确定类型)
- 如果在创建对象是没有指定类型,默认为Object
class Person<E>{ private E name; public Person(E name) { this.name = name; } public E getName(){ return name; } public void setName(E name){ this.name = name; }}
自定义泛型接口
- 接口中,静态成员也不能使用泛型(和泛型类规定一样)
- 泛型接口的类型,在继承接口和实现,接口时确定
- 没有指定泛型类型,默认为Object
public interface GenericInter<T,K> { T method(T name,K age);}
泛型方法
-
泛型方法可以定义字普通类中,也可以定义在泛型类中
class People{ private String name; private int age; public <K,V> void setName(K k,V v ){ System.out.println(k.getClass()); System.out.println(v.getClass()); }}public class Person<T,K> { public static void main(String[] args) { } public<U,M> void method(U u,M m){ }}
-
泛型方法在调用时,泛型类型就会确定下来,编译器会根据传入的参数来确定泛型类型
-
泛型方法可以使用类声明的方法,也可以使用自己声明的泛型
-
修饰符后没有使用泛型的方法,不是泛型方法。只是使用了泛型类的泛型
//不是泛型方法public void method(T t){ }
泛型的继承和通配
-
当确定泛型类型时,不能有继承的关系
//这样写是错的ArrayList<Object> objects = new ArrayList<String>();
-
泛型的通配(用在方法中约束参数的类型)
<?>
:支持任意泛型<? extends A>
:支持A类,即A类的子类,规定了泛型的上限(上限为A类)<? super A>
:支持A类,即A类的父类,规定了泛型的下限(下限为A类)
多线程
相关概念
-
程序:简单来说,就是我们写的代码
-
进程:运行中的程序,一个运行中的程序,就是一个进程。进程是程序的一次执行过程,或正在运行的一个程序。是动态的过程:有它自身的产生,存在和消亡的过程
-
线程:线程是由进程创建的,是进程的一个实体,一个进程可以允许有多个线程
-
并发:同一时刻,多个任务交替执行,造成一种同时运行的错觉,简单来说,单核CPU实现多任务的并发
-
并行:同一时刻,多个任务同时运行。多个CPU可实现并行
线程创建
继承Thread类
-
当一个类继承了Thread类后,该类就可以当作线程类使用
-
通常会重写
run()
,写上自己的业务代码。run()
定义在Runable
接口中 -
Thread类中的
run()
@Override public void run() { if (target != null) { target.run(); } }
线程的执行机制
public class TestThread { public static void main(String[] args) { Cat cat = new Cat(); cat.start(); }}class Cat extends Thread{ @Override public void run() { System.out.println("Hello Thread"); }}
-
当我们在IDEA中点击Run运行一个程序的时候,就相当于启动了一个进程,然后我们找到
main()
方法程序入口就相当于进程创建了一个子线程(main线程
) -
cat.start()
相当于main线程
启动了cat
线程 -
当main线程创建了cat线程后,不会阻塞main线程
-
**当我们所有的线程都运行结束,程序(进程)才会退出运行。**并不是主线程结束后,进程就结束了。
为什么启动线程不直接调用
run()
,而是调用start()
当我们调用cat.run()
,此时的run()就相当于一个普通的方法调用。调用该方法的的线程是main线程
,整个执行的过程,相当于串行执行,不会交替进行。只有调用了start()方法后,才是真真的启动了一个线程。
源码分析
public synchronized void start(){ start0();}/*start0() 是一个本地方法,是由JVM调用的真正事项多线程的是start0(),而不是run()*/private native void start0();
实现Runable接口
public class TestThread { public static void main(String[] args) { Dog dog = new Dog(); Thread thread = new Thread(dog); thread.start(); }}class Dog implements Runnable{ @Override public void run() { System.out.println("Hello "); }}
-
在java中是单继承机制,当一个类继承了其他类,就不能继承Thread类。所以提供了实现Runable接口的线程创建方式,这里采用的代理模式(静态代理)
-
在Runable接口中是没有start()方法的,要采用以下方式:
Dog dog = new Dog();Thread thread = new Thread(dog);thread.start();public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); }
-
静态代理的简单实现
public class ThreadProxy implements Runnable { private Runnable target = null; @Override public void run() { if(target != null){ target.run(); } } public void start(){ start0(); } private void start0(){ run(); } public ThreadProxy(Runnable target) { this.target = target; }}class Tiger implements Runnable{ public static void main(String[] args) { Tiger tiger = new Tiger(); ThreadProxy threadProxy = new ThreadProxy(tiger); threadProxy.start(); } @Override public void run() { System.out.println("HELLO"); }}
继承Thread类和实现Runable接口的区别
- 从Java设计来看,两种创建线程的本质是一样没有区别的,都是调用start(),再调用start()
- 事项Runable接口方式更加适合多个现象共享一个资源的情况,并避免了单继承的限制
常用方法
yield()
:让出CPU,让线程开始争夺CPUjoin()
:必须先把该线程执行完,再执行其他线程
public class TestMethod { public static void main(String[] args) throws InterruptedException { Person1 person1 = new Person1(); Thread thread = new Thread(person1); thread.setName("zzh"); thread.setPriority(Thread.MIN_PRIORITY); thread.start(); System.out.println("主线程休息1秒"); for (int i = 0; i < 10; i++) { System.out.println(i); Thread.sleep(100); if(i == 5){ thread.join(); } } thread.interrupt(); }}@SuppressWarnings({"all"})class Person1 implements Runnable{ @Override public void run() { boolean loop = true; int count = 0; while(loop){ System.out.println("HELLO" +Thread.currentThread().getName()+ " "+(++count)); try { Thread.sleep(100); } catch (InterruptedException e) { System.out.println("线程被中断了"); } if (count == 10){ break; } } }}
设置守护线程
-
用户线程:当线程的任务完成,或通知线程结束,线程就会结束
-
守护线程:一般为工作线程服务,当所有的用户线程执行结束,守护线程会自动结束。常见的守护线程:垃圾回收机制
thread.setDaemon(true);
通知线程
- 通过改变ture
-->
false,达到通知线程的效果
线程的状态
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6N6Jn3t1-1632385272629)(D:\document\小林code\小林图片\线程七大状态.png)]
线程同步
Synchronized(互斥锁)
- Java语言中,引入了对象互斥锁的概念,来保证共享数据的完整性
- 每个对象都对应一个互斥锁的标记,这个标记保证在任意时刻,只有一个线程访问该对象
- 关键字synchronized来与对象的互斥锁联系。当某个对象被synchronized修饰时,就表明该对象在任意时刻只有一个线程访问
- 同步的局限性:会导致程序的执行效率降低
- 同步方法(非静态)的锁可以是对象本身(this),也可以是其他对象(要求是同一对象)
- 同步方法(静态)的锁为当前类的本身
/* public synchronized void sell(){} 就是一个同步方法,这时的对象锁为this */ public synchronized void sell(){ if(count<=0){ System.out.println("售票结束"); loop = false; return; } System.out.println(Thread.currentThread().getName()+"售出第"+(count--)+"张票"); try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } //和上面的效果是一样的 public void sell(){ synchronized(this) { if (count <= 0) { System.out.println("售票结束"); loop = false; return; } System.out.println(Thread.currentThread().getName() + "售出第" + (count--) + "张票"); try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } } //静态方法的代码互斥锁 public static void method(){ synchronized (SellTicket.class){ System.out.println("HELLO"); } }
释放锁
可以释放锁的操作:
- 当前线程的同步代码块,同步方法执行完毕
- 当前线程在同步代码块中遇到break,return
- 当前线程同步代码块,同步方法中出现了未处理的Error或Exception,导致程序因异常结束
- 当前线程在同步代码块,同步方法中执行了线程对象的
wait()
,当前线程暂停,并释放锁
下面的操作不会释放锁:
- 线程执行同步代码块,同步方法时,程序调用
Thread.sleep()
,Thread.yield()
方法 暂停当前线程的执行,不会释放锁 - 线程执行同步代码块,其他线程调用了该线程的
supend()
方法将该线程挂起,该线程不会释放锁
文件
文件流
- 输入流:把文件的数据写入到内存中
- 输出流:把内存的数据写入到文件中
输入流和输出流是相对于内存而言的
创建文件
/*new File() 只是在内存中创建了File对象真正的创建文件到磁盘,必须要调用createNewFile() 方法*/File file = new File("d:\\document\\new.txt");file.createNewFile();File file1 = new File("d:\\document\\", "new1.txt");file1.createNewFile();
常用用文件方法
File file = new File("D:\\document\\new.txt");System.out.println(file.getName());System.out.println(file.getAbsolutePath());System.out.println("文件的大小"+file.length());System.out.println(file.exists());System.out.println(file.getParent());System.out.println(file.isFile());System.out.println(file.isDirectory());file.delete();//目录操作File file1 = new File("D:\\document\\demo\\a");//创建多级目录:可以同时创建多个目录if(file1.mkdirs()){ System.out.println("创建成功");}else{ System.out.println("创建失败");}file1.delete();//file1.mkdir():创建但级目录
IO体系图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GX8BkBoC-1632385272630)(D:\document\小林code\小林图片\IO体系图.png)]
- 字节流的顶级类:InputStream,OutputStream都是抽象类
- 字符流的顶级类:Writer,Reader都是抽象类
InputStream
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mmnumHst-1632385272632)(D:\document\小林code\小林图片\InputStream体系图.png)]
public void readFile() throws IOException { File file = new File("D:\\document\\new1.txt"); FileInputStream fileInputStream = new FileInputStream(file); int readDate = 0; //fileInputStream.read() 返回的其实是ASCLL码值 while((readDate = fileInputStream.read()) != -1){ System.out.print((char)readDate); } fileInputStream.close(); }public void readFile1() throws IOException { File file = new File("D:\\document\\new1.txt"); FileInputStream fileInputStream = new FileInputStream(file); int readDate = 0; byte[] buf = new byte[8]; //fileInputStream.read() 返回实际读取的字节数 //返回-1 表示读取完毕 while((readDate = fileInputStream.read(buf)) != -1){ System.out.print(new String(buf,0,readDate)); } fileInputStream.close(); }
OutputStream
public void writeFile() throws IOException { FileOutputStream fileOutputStream = new FileOutputStream("D:\\document\\new1.txt"); //方式一 fileOutputStream.write('Z'); //方法二 String str = "Hello,Word"; fileOutputStream.write(str.getBytes(StandardCharsets.UTF_8)); //方法三 fileOutputStream.write(str.getBytes(StandardCharsets.UTF_8),0,str.length()); //上面的三种方式都会覆盖原来的内容,实现添加的方式: FileOutputStream fileOutputStream1 = new FileOutputStream("D:\\document\\new1.txt",true); fileOutputStream.close();}
节点流和处理流
节点流:可以从某个特定的数据源(存放数据的地方)读取数据,如FileReader,FileWiter,FileInputStream,FileOutputStream,CharArrayRead(数组的节点流),PipedRead(管道节点流)。
处理流(包装流):对节点流进行包装,相当于一种缓冲。可以对任意的节点流进行包装
//构造器就是传入一个Witerpublic BufferedWriter(Writer out) { this(out, defaultCharBufferSize); }//public BufferedReader(Reader in) { this(in, defaultCharBufferSize); }
联系和区别
- 节点流是底层流/低级流,直接和数据源相连
- 处理流(包装流)用来包装节点流,既可以消除不同节点流的实现差异,也可以更方便的方法来完成输入输出
- 处理流使用了修饰器设计模式,不会直接与数据源相连
修饰器设计模式
public class Adapter { private Reader reader; public void setReader(Reader reader) { this.reader = reader; } public Reader getReader() { return reader; } public static void main(String[] args) { Adapter adapter = new Adapter(); StringReader stringReader = new StringReader(); FileReader fileReader = new FileReader(); adapter.setReader(stringReader); adapter.getReader().readString(); adapter.setReader(fileReader); adapter.getReader().readFile(); }}abstract class Reader{ public void readString(){} public void readFile(){}}class StringReader extends Reader{ @Override public void readString() { System.out.println("读取字符串"); }}class FileReader extends Reader{ @Override public void readFile() { System.out.println("读取文件"); }}
处理流的功能主要体现在以下两个方面
- 性能的提高:主要以增加缓冲的方式。来提高输入和输出的效率
- 操作便捷:处理流可以提供一系列便捷的方法来一次的输入和输出大批量的数据,是更加灵活方便
BufferedReader
-
BufferedReader是以字符为单位读取的,所以一般用于文本文件的读取。不适合二进制文件的读取。
-
关闭流时,只要关闭外层流即可,底层代码如下:
public void close() throws IOException { synchronized (lock) { if (in == null) return; try { //这里的关闭了底层流 in.close(); } finally { in = null; cb = null; } } }
BufferedWriter
@Test public void testBufWriter() throws IOException { FileWriter fileWriter = new FileWriter("D:\\document\\new2.txt",true); BufferedWriter bufferedWriter = new BufferedWriter(fileWriter); bufferedWriter.write("Hello,zzh"); bufferedWriter.newLine(); bufferedWriter.write("Hello,guogai"); bufferedWriter.close(); }
- 想要实现文件的追加写,需要在节点流处设置为
true
bufferedWriter.newLine();
实现换行
BufferedInputStream
public void PicCopy() throws IOException { FileInputStream fileInputStream = new FileInputStream("D:\\document\\小林code\\小林图片\\InputStream体系图.png"); FileOutputStream fileOutputStream = new FileOutputStream("D:\\document\\小林code\\Copy.png"); BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream); BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream); byte[] bytes = new byte[1024]; int len; while((len = bufferedInputStream.read(bytes)) != -1){ bufferedOutputStream.write(bytes,0,len); } bufferedInputStream.close(); bufferedOutputStream.close(); }
对象流
- 对象流(ObjectInputStream,ObjectOutputStream)都属于处理流
- ObjectInputStream提供序列化功能
- ObjectOutputStrem提供返序列化功能
序列化和反序列化
- 序列化就是保存数据时,保存数据的值和数据类型
- 反序列化就是恢复数据时,恢复数据的值和数据类型
- 需要让某个类支持序列化机制,则必须让该类是可序列化的。为了让某个类是可序列化的,该类必须实现两个接口之一:
- Serializable :这是一个标记接口,没有方法
- Externalizable
- 读取(反序列化)的顺序需要和保存数据(序列化)的顺序一致,否则会报异常
- 序列化和反序列是一一对应的的,版本问题
- 想要使用Dog中方法,就需要把Dog的定义类,放到可以访问的位置
public class TestObjectStream { public static void main(String[] args) throws IOException { FileOutputStream fileOutputStream = new FileOutputStream("D:\\document\\data.dat"); ObjectOutputStream oos = new ObjectOutputStream(fileOutputStream); oos.writeInt(100); oos.writeBoolean(true); oos.writeDouble(9.9); oos.writeUTF("HELlO,周志浩"); oos.writeObject(new Dog("erha",10)); oos.close(); } @Test public void Unser() throws IOException, ClassNotFoundException { FileInputStream fis = new FileInputStream("D:\\document\\data.dat"); ObjectInputStream ois = new ObjectInputStream(fis); System.out.println(ois.readInt()); System.out.println(ois.readBoolean()); System.out.println(ois.readDouble()); System.out.println(ois.readUTF()); System.out.println(ois.readObject()); }}class Dog implements Serializable{ private String name; private int age; public Dog(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return "Dog{" + "name='" + name + '\'' + ", age=" + age + '}'; }}
- 序列化的类中建议添加SerialVersionUID,为了提高版本的兼容性
- 序列化对象是,默认将类中的所有所有属性都进行序列化,但处理 static 和 transient 修饰的成员
- 序列化对象时,要求里面的属性类型也要实现序列化接口
- 序列化具备可继承性,也就是如果某类已经实现了序列化,则它的子类也默认实现了序列化
标准输入输出流
System.in 是System类中的 public final static InputStream in = null;/* 编译类型:InputStream 运行类型:BufferedInputStream 表示的是标准的输入 键盘*/System.out 是System类中的 public final static PrintStream out = null;/* 编译类型:PrintStream 运行类型:PrintStream 表示的是标准的输出 显示器*/Scanner scanner = new Scanner(System.in);String next = scanner.next();System.out.println(next);
转换流
InputStreamReader
- Reader的子类,可以将InputStream(字节流)包装成(转换)Reader(字节流)
OutStreamReader
- Writer的子类,可以将OutputStream(字节流)包装成(转换)Writer(字节流)
当处理纯文本数据时,使用字符流的效率更高,并且可以有效解决中文问题,所以建议将字节流转换成字符流。可以在使用指定编码的方式(比如:UTF-8,GBK)
打印流
打印流只有输出流,没有输入流,构造器中可以传入文件,字符串,OutputStream。也就是说可以将数据打印到多个位置
PrintStream(字节打印流)
PrintStream out = System.out;//在默认情况下,PrintStream输出的数据的位置:标准输出,即显示器out.print("HELLO");//因为print的底层就是一个writer,所以可以直接调用writer 进行打印输出out.write("HELLO".getBytes(StandardCharsets.UTF_8));System.setOut(new PrintStream("D:\\document\\new.txt"));/** 不能使用上面的out输出,和System.out 不是同一个对象* */System.out.print("HELLO");out.close();
PrintWriter(字符打印流)
PrintWriter printWriter1 = new PrintWriter(System.out);printWriter1.write("你好,zzh");PrintWriter printWriter = new PrintWriter("D:\\document\\new.txt");printWriter.write("你好,zzh");//真正将数据写入的方法printWriter.close();
Properties类
-
专门用来读写配置文件的集合类
-
配置文件格式
键=值键=值
-
键值对不需要有空格,值不需要引号引起来。默认类型为String
常用方法
//创建对象Properties properties = new Properties();//加载指定配置文件properties.load(new FileReader("src\\mysql.properties"));//把k-v显示在控制台properties.list(new PrintStream(System.out));//根据key,获取对应的值String user = properties.getProperty("user");String pw = properties.getProperty("pw");System.out.println("用户为:"+user);System.out.println("密码为:"+pw);Properties properties = new Properties();/* * 创建键值对 * 如果文件中没有key,就是创建 * 有key,就是修改 * */properties.setProperty("user","周志浩");properties.setProperty("psw","10086");properties.setProperty("charset","utf-8");properties.setProperty("psw","8848");/* * 将k-v存储在文件中 * 使用字节流 FileWriter 写入文件的,汉字存储的时unicode码 * 使用字符流 FileOutputStream 写入文件,汉字存储的是汉字 * */properties.store(new FileWriter("src\\mysql2.properties"),null);properties.store(new FileOutputStream("src\\mysql2.properties"),null);System.out.println("保存文件成功");
常用的文件复制方式
//字节流的处理方式int n = 0; byte[] bytes = new byte[1024]; while((n = fileInputStream.read(bytes)) != -1){ fileOutputStream.write(bytes,0,n); }//字符流的处理方式int data = 0; char[] buf = new char[8]; while((data = fileReader.read(buf)) != -1){ System.out.print(new String(buf,0,data)); }//缓冲流的处理方式String str = ""; while((str = br.readLine()) != null){ bw.write(str); bw.newLine(); }
反射
反射机制允许在执行期间借助Reflection API取得任何类的内部信息(比如成员变量,构造器,成员方法),并能操作对象的属性和方法。反射在设计模式和框架底层都会用到
加载完类之后,就会在堆中产生一个Class类型的对象(一个类只有一个Class对象,也只会加载一次),这个对象包含了类的完整结构信息。通过Class类的对象得到类的结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eGHKczWH-1632385272634)(D:\document\小林code\小林图片\Java程序运行三阶段.png)]
Java运行机制的作用
- 在运行时判断任意一个对象所属的类
- 在运行时构造任意一个类的对象
- 在运行时得到任意一个类所具有的成员变量和方法
- 在运行时调用任意一个对象的成员变量和方法
- 生成动态代理
反射的优点和缺点
优点:可以动态的创建和使用对象(也是框架的底层核心),使用灵活
缺点:使用反射基本是解释执行,对执行速度有影响
反射优化
Filed,Method,Constructor对象都有setAccessible()
,该方法是启动或禁用访问安全检查的开关,参数为true时,表示对象访问时取消安全检查情况。(优化效果不是特别明显)
Class类
-
Class类也是类,因此也继承Object类
-
Class类不是new出来的,而是系统创建的。有ClassLoader类中的
loadClass(String name)
创建public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); }
-
对于某个类的Class对象,在内存中只有一份,因为类只加载一次
-
每个类的对象都会记得自己是由那个对象实例生成的
-
通过Class对象的各种API,可以完整得到一个类的完整结构
-
Class对象存放再堆中
-
类的字节码二进制数据,是放在方法区的,有的地方称为类的元数据(包括方法代码,变量名,方法名,访问权限)[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KlAj5eBH-1632385272635)(D:\document\小林code\小林图片\继承内存关系图.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sCoSikz2-1632385272636)(D:\document\小林code\小林图片\类加载内存.png)]
常用方法
String fullpath = "reflection.Person";Class<?> aClass = Class.forName(fullpath);//输出aClass对象,属于那个了类 class reflection.PersonSystem.out.println(aClass);//输出aClass对象的运行类型System.out.println(aClass.getClass());//得到包名System.out.println(aClass.getPackage().getName());//创建类的对象实例Person person = (Person) aClass.newInstance();System.out.println(person);//通过反射获取属性 该方法不能获取私有属性Field name = aClass.getField("name");//public java.lang.String reflection.Person.nameSystem.out.println(name);//nameSystem.out.println(name.getName());//zzhSystem.out.println(name.get(person));//通过反射设置属性值name.set(person,"erha");System.out.println(name.get(person));Field[] fields = aClass.getFields();for (Field f : fields) { System.out.println(f.getName());}//无参构造器Constructor<?> constructor = aClass.getConstructor();System.out.println(constructor);//有参构造器Constructor<?> constructor1 = aClass.getConstructor(String.class, int.class);System.out.println(constructor1);
获取类对象的方法
//应用场景:配置文件String fullpath = "reflection.Person";Class<?> cls1 = Class.forName(fullpath);System.out.println(cls1.hashCode());//应用场景:传入参数Class<Person> cls2 = Person.class;System.out.println(cls2.hashCode());//应用场景:有对象实例Person person = new Person();Class<? extends Person> cls3 = person.getClass();System.out.println(cls3.hashCode());//通过类加载器获取类对象//(1) 先得到类加载器ClassLoader classLoader = person.getClass().getClassLoader();Class<?> cls4 = classLoader.loadClass(fullpath);System.out.println(cls4.hashCode());//基本数据类型获取Class类Class<Integer> integerClass = int.class;Class<Character> characterClass = char.class;//数据类型对应的包装类,可以通过.TYPE的Class对象Class<Integer> type = Integer.TYPE;Class<Character> type1 = Character.TYPE;
哪些类型有Class对象
- 外部类,成员内部类,静态内部类,局部内部类,匿名内部类
- interface:接口
- 数组
- enum:枚举
- annotation:注解
- 基本数据类型
- void
静态加载和动态加载
静态加载:编译时加载相关的类,也就是说编译时(转为字节码文件)就会把你代码中用到的类进行加载,如果没有就会报错,依赖性太强
- 创建对象时(new)
- 子类被加载,父类也被加载
- 调用类中的静态成员
动态加载:运行时加载需要的类,如果允许时没有用到该类,即使该类不存在,则不会报错,降低了依赖性
- 反射机制
类加载过程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pA8Bte0C-1632385272638)(D:\document\小林code\小林图片\类加载过程图.png)]
加载阶段(Loading)
- 是通过有ClassLoader类中的
loadClass(String name)
进行加载的 - JVM该阶段的主要目的是将字节码重不同的数据源(可能是class文件,jar包,甚至是网络)转化为二进制字节流加载到内存中,并生成一个代表该类的
java.lang.Class
对象
连接阶段
连接阶段,其实就是将类的二进制数据合并到JRE中,此时,已经是一个可执行文件
验证阶段
- 目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
- 包括文件格式验证(是否已魔数 oxcafebabe开头),元数据验证,字节码验证和符号引用验证
- 可以考虑使用 Xverify:none 参数来关闭大部分的类验证措施,缩短虚拟机类加载的时间
准备阶段
-
给静态变量分配内存并且完成默认初始化
/** n1 是实例属性,不是静态变量 ,因此在准备阶段,是不会分配内存的* n2 是静态变量,在准备阶段,分配内存n2,默认初始化为 0* n3 是静态常量,他和静态变量不同,一旦复制就不会在变,所以默认初始化为 40* */private int n1 = 20;private static int n2 = 30;private static final int n3 = 40;
解析阶段
- JVM将会把常量池中的符号引用(一种特别的关系)替换为直接引用(内存地址)
初始化阶段
- 执行静态代码块,静态变量的指定赋值
- 到了初始化阶段,才开始真正的执行Java代码,此阶段是执行`clint<>()方法的过程
clint<>()
是由编译器按语句在源文件中出现的顺序,依次自动收集类中所有静态变量的赋值和静态代码块中的语句,并进行合并clint<>()
会有同步的过程,保证同一时刻只有一个线程加载该类。正是因为这个机制才能保证在内存中只有一个Class类对象
最后内存的分布情况:
- 以二进制形式将字节码放入到方法区
- 在堆区生成字节码对应的数据结构,也就是类的Class对象
加载阶段和连接阶段是由JVM来负责的,初始化阶段程序员时可以控制的
获取类的结构信息
String fullpath = "reflection.Cat";Class<?> catClass = Class.forName(fullpath);//获取全类名System.out.println(catClass.getName());//获取简单类名System.out.println(catClass.getSimpleName());//获取本类及父类中的所有public属性Field[] fields = catClass.getFields();for (Field field : fields) { //不使用getNam()的输出:public int reflection.Cat.size //使用getNam()的输出:size System.out.println(field.getName());}//获取本类中所有的属性Field[] declaredFields = catClass.getDeclaredFields();for (Field declaredField : declaredFields) { System.out.println(declaredField.getName());}//获取本类及父类的public方法Method[] methods = catClass.getMethods();for (Method method : methods) { System.out.println(method.getName());}//获取本类中所有的方法Method[] declaredMethods = catClass.getDeclaredMethods();for (Method declaredMethod : declaredMethods) {System.out.println(declaredMethod);}//获取本类所有的public构造器(并没有拿到父类的)Constructor<?>[] constructors = catClass.getConstructors();for (Constructor<?> constructor : constructors) { System.out.println(constructor.getName());}//获取本类所有的构造方法Constructor<?>[] declaredConstructors = catClass.getDeclaredConstructors();for (Constructor<?> declaredConstructor : declaredConstructors) { System.out.println(declaredConstructor.getName());}//获取包信息System.out.println(catClass.getPackage());//以Class形式返回父类信息System.out.println(catClass.getSuperclass());//返回接口信息Class<?>[] interfaces = catClass.getInterfaces();for (Class<?> anInterface : interfaces) { System.out.println(anInterface.getName());}//返回注解信息Annotation[] annotations = catClass.getAnnotations();for (Annotation annotation : annotations) { System.out.println(annotation);}
String fullpath = "reflection.Cat"; Class<?> catClass = Class.forName(fullpath); //获取本类中所有的属性 /* *获取修饰符时:默认修饰符 0 public 1 private 2 protected 4 static 8 final 16 * 有多个修饰符时,数字相加 * */ Field[] declaredFields = catClass.getDeclaredFields(); for (Field declaredField : declaredFields) { System.out.println("属性名:"+declaredField.getName() +" 属性访问修饰符"+declaredField.getModifiers() +" 属性的类型"+declaredField.getType()); } //获取本类中所有的方法 Method[] declaredMethods = catClass.getDeclaredMethods(); for (Method declaredMethod : declaredMethods) { System.out.println("方法名:"+declaredMethod +" 方法访问修饰符:"+declaredMethod.getModifiers() +" 方法返回类型:"+declaredMethod.getReturnType()); Class<?>[] parameterTypes = declaredMethod.getParameterTypes(); for (Class<?> parameterType : parameterTypes) { System.out.println("参数类型"+parameterType.getName()); } } //获取本类所有的构造方法 Constructor<?>[] declaredConstructors = catClass.getDeclaredConstructors(); for (Constructor<?> declaredConstructor : declaredConstructors) { System.out.println("构造器名:"+declaredConstructor.getName() +" 构造器访问修饰符"+declaredConstructor.getModifiers()); Class<?>[] parameterTypes = declaredConstructor.getParameterTypes(); for (Class<?> parameterType : parameterTypes) { System.out.println(parameterType.getName()); } }