java笔记
一、java基础
1、数据类型
- 基本数据类型
int :4字节
float :4字节
double :8字节
char :2字节
long :8字节
byte :1字节
boolen:被编译成 int 类型来使用,占 4 个 byte
short:2字节 - 引用数据类型
字符串
数组
类
接口
lambda
注意事项:
- 字符串是引用数据类型;
- 浮点数可能只是一个近似值,并非精确,所以两个相同的浮点数使用==可能为false;
- 浮点数默认为double类型,如果使用float需要加后缀F;整数默认为int类型,如果使用long需要加后缀L;
- 基本数据类型都有包装类型,基本类型与包装类型之间的赋值使用自动装箱和拆箱完成。
2、数据类型转换
隐式类型转换(小范围转大范围)
- char/byte/short 类型在计算时会自动转换为int类型
- +=,-=,*=,/=运算符可以执行隐式类型转换
- String + int = String
short s1 = 1;
// s1 = s1 +1; 报错,因为=右边已经是int类型了
s1+=1; //隐式类型转换
s1 = (short) (s1 + 1); //强制类型转换,编译通过
强制类型转换(大范围转小范围)
- 小范围类型=(小范围类型)原本类型
3、缓冲池
编译器自动装箱过程中,会优先从缓冲池中寻找对象,如果缓冲池中没有再创建新的对象。
基本数据类型对应的缓冲池:
- boolean : true、false
- byte :所有值缓冲池都有
- short :-128 ~ +127
- int :-128 ~ +127
- char: \u0000 ~ \u007F
注意事项:Integer.valueOf(123)函数也会先在缓冲池中寻找
Integer x = new Integer(123);
Integer y = new Integer(123);
System.out.println(x == y); // false
Integer z = Integer.valueOf(123);
Integer k = Integer.valueOf(123);
System.out.println(z == k); // true
4、String类型
特点:
1.被声明为final,因此不可继承
2.内部使用char数组存储,该数组被声明为final,意味着 value 数组初始化之后就不能再引用其它数组,因此可以保证 String 不可变。
不可变的好处:
1.可以缓存hash值,因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。
2.String Pool 的需要,如果一个String对象被创建过了,就会从字符串常量池中取得。
3.安全性,String 经常作为参数,String 不可变性可以保证参数不可变。
4.线程安全,可以在多个线程中安全使用。
String string1 = "hello"
String string2 = "hello"
System.out.print(string1==string2);//输出为true,即为同一个引用对象
String string1 = new String("hello");
String string2 = new String("hello");
System.out.print(string1==string2);//输出为false,不同对象
String, StringBuffer and StringBuilder的区别:
1.String不可变,StringBuffer and StringBuilder可变
2.String和StringBuffer线程安全,StringBuilder线程不安全
String常用方法:
boolean equals( String str );//字符串内容一样则返回true,和常量比较时推荐 “abc”.equals(string); “ == ”比较的是地址
public int length( String str );//返回长度
public String concat(String str1,String str2);//返回字符串拼接,原字符串不变
public char charAt( int index );//返回字符串某个字符
public int indexOf( String str);//返回某个字符串在本字符串中出现的位置,没有则返回-1
public String substring(int index);//返回截取参数到结尾的字符串
public String substring(int begin, int end );//返回[ begin, end )之间的字符串
//与转换有关的方法:
public char[ ] toCharArray( );//转化为字符数组
public byte[ ] getBytes( );//获取底层字节数组
public String replace( charSequence str1, charSequence str2 );//字符串的替换
//分割字符串的方法:
public String[ ] split( String regex );//返回分割后的字符串数组,注意:参数其实是一个正则表达式,如果按“.”分割,则需在参数中写(“\\.”)
5、数组
5.1 初始化
静态初始化:
int[] array2 = new int [ ]{"ha","lou"};
int[] array3 = {"ha","lou"}; //(不能拆成两个步骤)
动态初始化: 初始化时,存数据类型的默认值
int[] array1 = new array [5];
注意: 直接打印数组内存名,会得到对应的内存地址哈希值
5.2 数组作为参数传递
- 数组作为返回值,返回的是数组地址。
- 数组作为方法参数,传入的也是数组地址。事实上,java中所有参数传递的都是值传递。
public static int[ ] arrayName ( int [ ] array1 ) { return array1 ;}
5.3 多维数组
以二维数组为例:
创建一个二维数组:
int[][] array = new int[3][4];
本质是创建了一个大小为3的一维数组,该数组的每一个元素都是一个大小为4的一维数组
line = array.length;//返回二维数组行数
colum = array[0].length;//返回二维数组的列数
6、java内存划分
1.栈(stack):存放方法中的参数,即局部变量,超出作用域则消失;方法的运行一定在栈中;
2.堆(heap):凡是new出来的东西都存放在堆当中;堆内存中的东西都有都有默认值:其中布尔类型默认是false,字符类型默认是’ \u0000 ‘,int类型默认为0
3.方法区(method area):存储.class相关信息,包含方法的信息。
4.本地方法栈(native method stack):与操作系统有关。
5.寄存器(pc register):与CPU有关。
7、程序在内存中的存放位置及关系
示例1:简单的数组对象访问
public class Main{
public static void main(String[] args){
int[] array = new int[3]; //动态初始化
System.out.println(array); //地址值
System.out.println(array[0]); //0
System.out.println(array[1]); //0
System.out.println(array[2]); //0
System.out.println("=========================")
array[1]=10;
array[2]=20;
System.out.println(array); //地址值
System.out.println(array[0]); //0
System.out.println(array[1]); //10
System.out.println(array[2]); //20A
}
}
示例2:数组对象引用
public class Main{
public static void main(String[] args){
int[] arrayA = new int[3]; //动态初始化
arrayA[1]=10;
arrayA[2]=20;
int[] arrayB = arrayA;
System.out.println(arrayB); //地址值
System.out.println(arrayB[0]); //0
System.out.println(arrayB[1]); //10
System.out.println(arrayB[2]); //20
arrayB[1] = 100;
arrayB[2] = 200;
System.out.println(arrayB[0]); //0
System.out.println(arrayB[1]); //10
System.out.println(arrayB[2]); //20
}
}
示例3:创建对象
public class Phone{
String brand;
double price;
public void call(String who){
System.out.println("call "+who);
}
public void sendMessage(){
System.out.println("发送短息 ");
}
}
public class Main{
public static void main(String[] args){
Phone one = new Phone();
System.out.println(one.brand);
System.out.println(one.price);
one.brand = "苹果";
one.price = 12000;
System.out.println(one.brand);
System.out.println(one.price);
one.call("乔布斯");
one.message();
}
}
例4:对象作为参数传递
public class Main{
public static void main(String[] args){
Phone one = new Phone();
one.brand = "苹果";
one.price = 12000;
method(one);
}
public static void method(Phone phone){
System.out.println(one.brand);
System.out.println(one.price);
}
}
8、java的三大特性
8.1 封装
含义: 利用抽象数据类型将数据和操作封装在一起,使其构成一个不可分割的独立实体。尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。
优点:
- 减少耦合:各部分能够独立开发
- 减轻维护负担
- 有效调节性能
- 提高可重用性
- 降低了构建大型系统的风险:即使整个系统不可用,但是这些独立的模块却有可能是可用的
8.2 继承
含义: 子类得到父类的一些方法和属性。
注意:
- 继承应该遵循里氏替换原则,子类对象必须能够替换掉所有父类对象。
- java为单继承,即子类只能继承一个父类
- 父类引用指向子类对象称为向上转型
访问权限:
Java 中有三个访问权限修饰符: private、protected 以及 public,如果不加访问修饰符,表示默认default。权限大小:public>protect>default>private
public:公共权限,任何类或方法调用该类都可以使用
protect:保护权限,只有该类和该类的子类可以访问
default(默认不写):默认权限,同一个包里的类可以访问
private:私有权限,只能该类访问
继承中构造方法的访问特点:
- 子类构造方法中默认有一个
super()
调用,所以一定是先调用父类的构造,再调用子类的构造。注:如果父类没有无参构造方法会报错。
- 子类必须调用父类的构造方法,不写则默认添加
super()
;写了,则用指定的super()
调用 - 在本类的构造方法中,访问本类的另一个构造方法,
this()
也必须是构造方法的第一个语句,且唯一,且不能和super()
同时使用
继承中成员变量的访问特点:
在父子类的继承关系中,如果成员变量重名,则创建子类对象时,访问方法有两种方式——
- 直接通过子类对象访问成员变量:等号左边是谁,就优先使用谁,没有则向上找
- 间接通过成员方法访问访问成员变量:该方法属于谁就优先使用谁,没有则向上找
8.3 多态
多态分为编译时多态和运行时多态:
- 编译时多态主要指方法的重载
- 运行时多态:父类引用指向的子类对象的具体类型在运行期间才确定
多态的使用:
父类名称 对象名 = new 父类名称();
接口名称 对象名 = new 实现类名称();
访问成员变量的两种方式:
1、 直接通过对象名称访问成员变量,等号左边是谁,优先使用谁,没有则向上查找
2、通过成员方法访问成员变量,看方法属于谁,优先使用谁,没有则向上查找
成员方法的访问规则:
1、 看new的谁,就优先使用谁,没有则向上查找(因为new的对象方法同名方法会进行覆盖)
对象的上下转型:
instanceof关键字
通过instanceof关键字,可以知道父类引用的对象本来是什么子类
格式: 对象 instanceof 类名 ,将会得到一个boolean类型值结果,判断前面的对象能不能当后面类型的实例。
9、重写和重载
9.1 重写(Override)
存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法,以用@override检测覆盖是否成功,重写有以下限制:
- 必须保证父子类之间的方法名称相同,参数列表相同
- 子类方法的权限必须【大于等于】父类方法的权限修饰符,私有方法和静态方法无法覆盖重写
- 子类方法的返回类型必须是父类方法返回类型或为其子类型
9.2 重载(Overload)
存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同【方法名相同,参数列表不同】
注意: 与返回值没有关系,返回值不同,其它都相同不算是重载。
10、局部变量和成员变量区别
- 定义位置不一样:成员变量在类当中,局部变量在方法内
- 作用范围不一样:成员变量在整个类中通用,局部变量只能在方法内使用
- 默认值不一样:成员变量有默认值,局部变量没有必须创建时声明
- 内存位置不一样:成员变量在堆中,局部变量在栈中
- 生命周期不一样:成员变量随着对象的创建而诞生,随着对象被收回而消失,局部变量随着方法进栈而诞生,随着方法出栈而消失。
11、常用类
11.1 Scanner类
import java.util.Scanner; //导包
scanner sc = new Scanner(System.in); //创建Scanner类
sc.next(); //键入字符串
sc.nextInt();
sc.nextDouble(); //键入数字
11.2 Random类
import java.util.Random; //导包
Random r = new Random( ); //创建
int num = r.nextInt( ); //使用
int num = r.nextInt(int n ); //产生[0, n)的随机数
11.3 Array数组类
import java.util.Array;
public static String toString( 数组 );//将数组装化为字符串
public static void sort( 数组 );//将数组默认升序排序,如果是自定义类需要有(Comparable或comparactor接口支持)
11.4 ArrayList集合类
和数组的区别: arraylist集合的长度是可变的,而数组长度是不可变的
ArrayList<String> list = new ArrayList<>(); //创建一个list对象
list.add(E e ); //添加元素
// 常用方法:
public Boolean add(E e); //备注:对于Arraylist对象而言,add()一定成功,返回值可有可无
public E get(int index);
public E remove(int index);
public int size( );
注意事项:
- 对于ArrayList集合来说,直接打印得到的不是地址值,而是内容,为空时输出[ ]
- 对于ArrayList来说,有一个代表泛型(泛型:装在集合里的元素全是统一的该类型,泛型只能是引用类型,不能是基本类型)
- 如是希望在ArrayList中存储基本数据类型,则必须使用基本类型对应的“包装类”
byte/byte 、short/Short、 int/Integer、 long/Long 、 double/Double、
char/Character、 boolean/Boolean - ArrayList只适合用来遍历查找,而删除操作效率低
12、抽象类和抽象方法
抽象方法:加上abstract
关键字,不需要{ }
实现任何功能。
抽象类:在class之前加上abstract
关键字。抽象方法必须在抽象类中,抽象类中可以有具体方法。
注意事项:
- 不能直接new抽象类
- 必须由一个子类来继承抽象父类
- 子类必须覆盖重写(实现)抽象类中的所有的抽象方法,然后创建子类对象进行使用;如果没有实现所有的抽象方法,则子类仍是一个抽象类
- 覆盖重写(实现):去掉抽象方法的abstract关键字,补上方法体
13、接口
定义: 接口就是多个类的公共规范;接口是一种引用数据类型,最重要的就是其中的抽象方法。注:关键字变成interface之后,其生成的字节码文件仍然是.class
public interface 接口名{
//接口内容
}
注意事项:
- 接口中的抽象方法,修饰符必须是两个固定的关键字——
public abstract
,也可省略 - 接口中包含的内容有:1.常量(默认都是static 、 final修饰) 2.抽象方法 3.默认方法 4.静态方法 5.私有方法(JDK9以后)
接口的使用:
1、 接口不能直接使用,必须定义一个实现类来实现该接口;
public class 实现类名称 implement 接口名{
……
}
2、 接口的实现类必须覆盖重写(实现)接口中的所有抽象方法,除非实现类是抽象类;
3、 创建实现类的对象,进行使用。
接口中的默认方法(jdk8以后)
1、接口中使用默认方法,可以解决接口升级问题;
2、接口中的默认方法可以通过实现类对象直接调用,也可以被实现类进行覆盖重写。
public default 返回值类型 方法名( ){
//方法体
}
接口中的私有方法(jdk9以后)
用途: 接口中定义一个方法,内部解决两个默认方法之间代码复用的问题,API客户无法看到也就不能被实现类调用。
- 普通私有方法:解决多个默认方法之间代码重复问题
private 返回值类型 方法名称(){
//方法体
}
- 静态私有方法:解决多个静态方法之间代码重复问题
private static 返回值类型 方法名称(){
//方法体
}
接口中的常量——推荐完全大写+下划线_命名
必须使用public static final
进行修饰,从效果上来看,其实就是接口的常量。即使省略了关键字,仍然是常量,必须手动赋值,不可更改。
使用接口时注意:
1、 接口是没有静态代码块或者构造方法的;
2、 一个类的直接父类是唯一的,但是一个类可以同时实现多个接口;
public class DemoInterface implements MyInterface1, Myinterface2{
//覆盖所有的抽象方法
}
3、 实现类所实现的所有接口中,存在重复的抽象方法,那么只需覆盖重写一次即可;
4、 实现类若没有完全覆盖所有的抽象方法,那么该实现类必须是一个抽象类;
5、 如果实现类所实现的多个接口当中,存在重复默认方法,则实现类一定要对重复的默认方法进行覆盖重写,而且带着default关键字不能省;
6、 直接父类中的方法和所实现接口的默认方法产生冲突,优先使用父类中的方法(继承优先于实现)
14、关键字
14.1 static 关键字
- 一旦使用了static关键字,那么这样的内容将不再属于对象自己,而是属于类的。凡是本类的对象,都共享同一份。
- 无论是静态成员变量还是成员方法都推荐使用类名来调用。(即使用了对象名,编译器也会把它翻译过来)
- 对于本类中的静态方法,可以忽略类名
- 静态不能访问非静态(因为【先】有静态内容,【后】有非静态内容)
- 静态方法不能用this关键字(this表示当前对象)
- 静态方法存放在方法区的静态区
静态代码块
当第一次用到本类时,静态代码块执行唯一一次;总是优先于构造方法
public class 类名称{
static{
//静态代码块内容
}
}
静态内部类:
非静态内部类依赖于外部类的实例,而静态内部类不需要。静态内部类不能访问外部类的非静态的变量和方法。
// 静态导包,在使用静态变量和方法时不用再指明 ClassName,从而简化代码
import static com.xxx.ClassName.*
public class OuterClass {
class InnerClass {
}
static class StaticInnerClass {
}
public static void main(String[] args) {
// InnerClass innerClass = new InnerClass(); // 'OuterClass.this' cannot be referenced from a static context
OuterClass outerClass = new OuterClass();
InnerClass innerClass = outerClass.new InnerClass();
StaticInnerClass staticInnerClass = new StaticInnerClass();
}
}
14.2 final 关键字
四种用法:
1、 修饰一个类 ———— 不能有任何子类(太监类 )
2、 修饰一个方法 ———— 这个方法不能被子类覆盖重写
3、 修饰一个成员变量 ———— 只能一次赋值,内容不可更改;初始化和赋值可以分阶段进行
4、 修饰一个局部变量 ———— 变量不可变,由于成员变量具有默认值,所以final之后必须手动赋值;对于final成员变量,可以直接赋值或构造方法赋值(选其一)
注意: 对于方法、类来说,abstract 和 final关键字矛盾,不能同时出现
对于基本类型来说,指内容不变,对引用类型来说,指地址不变(但是内容可变)
15、内部类
含义: 一个事物的内部包含另一个类,包括 成员内部类和局部内部类(匿名内部类)
成员内部类的使用:
1.间接方法:在外部类中实例化一个内部类,通过外部类的方法调用
2.直接方法:外部类名称.内部类名称 对象名 = new 外部类名称( ).new 内部类名称( )
通过该方法之间创建一个内部类对象
注意: 如果内部类和外部类中的变量重名,则this.变量名
表示内部类中的变量,外部类.this.变量名
表示外部类中的变量。
局部内部类的使用:
1.只用当前所属方法内部可以使用它
2.在当前方法中实例化该类,然后使用该类,外部通过当前方法使用该类的实例方法。
注意: 如果希望访问所在方法的局部变量,那么这个局部变量必须是【有效final的】。(原因:内部类和局部变量的生命周期不一样)Java8+以后,只要局部变量不变,则final关键字可以省略。
匿名内部类的使用:
如果接口的实现类(或父类的子类)只需要使用唯一的一次,那么就可以省略该类的定义,使用匿名内部类。
接口名称 对象名 = new 接口名称( ){
@overide
//覆盖重写所有的抽象方法
}
16、Object类的通用方法
16.1 概览
public final native Class<?> getClass()
public native int hashCode()
public boolean equals(Object obj)
protected native Object clone() throws CloneNotSupportedException
public String toString()
public final native void notify()
public final native void notifyAll()
public final native void wait(long timeout) throws InterruptedException
public final void wait(long timeout, int nanos) throws InterruptedException
public final void wait() throws InterruptedException
protected void finalize() throws Throwable {}
16.2 equals()
等价关系:
1.自反性 x.equals(x)
2.对称性 x.equals(y) == y.equals(x)
3.传递性 if (x.equals(y) && y.equals(z)) x.equals(z)
4.一致性:多次调用结果不变 x.equals(y) == x.equals(y)
equals() 与 ==比较:
1.对于基本类型,==
判断两个值是否相等,基本类型没有equals()
方法
2.对于引用类型,==
判断两个变量是否引用同一个对象,而equals()
判断引用的对象是否等价
16.3 hashCode()
说明:
1.hashCode()
返回散列值,而equals()
是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价。
2.在覆盖 equals()
方法时应当总是覆盖 hashCode()
方法,保证等价的两个对象散列值也相等。HashSet等集合添加不重复对象时,判断的就是hashCode值。
16.4 toString()
说明:
1.默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示
2.方便对象内容打印。
16.5 clone()
说明:
1.clone()
是 object
的 protected
方法,一个类不显式去重写 clone(),则其它类就不能直接去调用。
2.重写clone()
方法时需要实现Cloneable
接口(规定),否则会抛出CloneNotSupportedException
异常
拷贝:
- 浅拷贝:拷贝对象和原始对象的引用类型引用同一个对象
- 深拷贝:拷贝对象和原始对象的引用类型引用不同对象
clone()的替代方案:
使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换,以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。
public class CloneConstructorExample {
private int[] arr;
public CloneConstructorExample() {
arr = new int[10];
for (int i = 0; i < arr.length; i++) {
arr[i] = i;
}
}
public CloneConstructorExample(CloneConstructorExample original) {
arr = new int[original.arr.length];
for (int i = 0; i < original.arr.length; i++) {
arr[i] = original.arr[i];
}
}
public void set(int index, int value) {
arr[index] = value;
}
public int get(int index) {
return arr[index];
}
}
CloneConstructorExample e1 = new CloneConstructorExample();
CloneConstructorExample e2 = new CloneConstructorExample(e1); //使用构造函数进行深拷贝
e1.set(2, 222);
System.out.println(e2.get(2)); // 结果是2
二、集合
1、容器
说明: 就是可以容纳其他Java对象的对象。优点是:
- 降低编程难度
- 提高程序性能
- 提高API间的互操作性
- 降低学习难度
- 降低设计和实现相关API的难度
- 增加程序的重用性
注意: Java容器里只能放对象,对于基本类型(int, long, float, double等),需要将其包装成对象类型后(Integer, Long, Float, Double等)才能放到容器里。
容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。
容器接口继承关系:
2、Collection
和数组的区别:
1.数组长度固定,而集合长度是可变的;
2.数组中存储的是同一类元素,可以存储基本数据类型的值;集合存储的都是对象,对象的类型可以不一致。
2.1 List接口
特点:
1.有序的集合
2.允许有重复的元素
3.有索引,可以使用普通的for循环遍历
2.1.1 ArrayList(线程不安全)
说明: ArrayList
底层是一个数组,使用自动扩容的机制避免超出容量。数组扩容通过一个公开的方法ensureCapacity(int minCapacity)
来实现,也可以根据需求手动增加容量。
常用函数:
add()
addAll()
set()
get()
remove()
trimToSize() //将底层数组的容量调整为当前列表保存的实际元素的大小的功能
indexOf(), lastIndexOf()
Fail-Fast机制: 记录modCount参数,在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险。
2.1.2 LinkedList(线程不安全)
说明: LinkedList
同时实现了List接口和Qeque接口,也就是说它既可以看作一个顺序容器,又可以看作一个队列(Queue),同时又可以看作一个栈(Stack)。注:关于栈或队列,现在的首选是ArrayDeque
特点: 底层是双向链表,增删快,查找慢
线程不安全,如果想让其安全,可以使用Collections.synchronizedList()
方法将其转换为一个SynchronizedList对象。
2.1.3 Vector(线程安全)
说明: 集合中的操作都是同步方法,线程安全,但是效率低。
SynchronizedList对象和Vector对象的一些区别:
1.SynchronizedList有很好的扩展和兼容功能。他可以将所有的List的子类转成线程安全的类。
2.使用SynchronizedList的时候,进行遍历时要手动进行同步处理。
3.SynchronizedList是同步代码块可以指定锁定的对象,Vector是同步方法
2.2 迭代器
说明: 对集合进行遍历,import java.util.Iterator
常用的两个方法:
boolean hasNext( ) //如果仍有元素,则返回true
E next( ) //返回迭代的下一个元素
迭代器的使用:
1、使用集合中的方法iterator( )
获取迭代器接口的实现类,使用Iterator接口接收(多态)
2、使用Iterator中的boolean hasNext( )
方法判断是否还有下一个元素
3、使用Iterator中的E next( )
方法取出集合中的下一个元素
增强for循环: 底层使用迭代器,简化了迭代器的使用
for(String str:collection){
System.out.println(str);
}
2.3 Queue&Stack
概述: Java里有Stack的类(Vector继承类),却没有Queue的类(它是个接口名字)。当需要使用栈时,Java已不推荐使用Stack
,而是推荐使用更高效的ArrayDeque
;使用队列时也首选ArrayDeque
(次选是LinkedList)。
2.3.1 Queue接口
Queue
接口继承自Collection接口,除了最基本的Collection的方法之外,它还支持额外的insertion, extraction和inspection操作。共6个方法,一组是抛出异常的实现;另外一组是返回值的实现(没有则返回null)。
2.3.2 Deque接口
Deque
是”double ended queue”, 表示双向的队列,继承自 Queue接口,除了支持Queue的方法之外,还可以对队列的头和尾都进行操作。
由于Deque
既可以当队列,也可以当栈使用,对应的方法分别为:
2.3.3 ArrayDeque类
是Deque
接口的实现类,底层通过循环数组实现,任何一点都可能被看作起点或者终点。线程不安全,另外不允许放入null
元素。
2.3.4 PriorityQueue类
是Queue
接口的实现类,是一种优先队列,作用是每次取出的元素都是队列中最小的元素(构造函数中需要传入比较器Comparator进行比较)。底层是由数组表示的小顶堆实现的。
PriorityQueue的peek()
和element()
操作是常数时间,add()
, offer()
, 无参数的remove()
以及poll()
方法的时间复杂度都是log(N)。
2.4 Set接口
特点:
1.元素不重复
2.没有索引
3.无序,即放入和取出的顺序可能不同
2.4.1 HashSet
是对HashMap
进行了简单封装实现的,也就是说HashSet里面有一个HashMap(适配器模式),具体内容参考HashMap
。
2.4.2 LinkedHashSet
是对LinkedHashMap
进行了简单封装实现的,也就是说LinkedHashSet里面有一个LinkedHashMap(适配器模式),具体内容参考LinkedHashMap
。
2.4.3 TreeSet
是对TreeMap
进行了简单封装实现的,也就是说TreeSet里面有一个TreeMap(适配器模式),具体内容参考TreeMap
。
3、Map
3.1 HashMap
特点:
1.线程不安全
2.不保证元素顺序
3.采用拉链法解决哈希冲突
底层结构:
注意: 将对象放入到HashMap或HashSet中时,需要重写hashCode()和equals()方法。
在java8以后,HashMap底层改由数组+链表+红黑树组成,当冲突元素大于8个时,会将冲突链表转化为红黑树,这时在这些位置查找的时间复杂度降为O(logN)。
3.2 LinkedHashMap
特点: HashMap的基础上,采用双向链表(doubly-linked list)的形式将所有entry对象连接起来,为保证元素的迭代顺序跟插入顺序相同。
LinkedHashMap经典用法:
可以轻松实现一个采用了FIFO替换策略的缓存。具体说来,LinkedHashMap有一个子类方法protected boolean removeEldestEntry(Map.Entry<K,V> eldest)
,该方法的作用是告诉Map是否要删除“最老”的Entry,所谓最老就是当前Map中最早插入的Entry,如果该方法返回true,最老的那个元素就会被删除。在每次插入新元素的之后LinkedHashMap会自动询问removeEldestEntry()
是否要删除最老的元素。这样只需要在子类中重载该方法,当元素个数超过一定数量时让removeEldestEntry()返回true,就能够实现一个固定大小的FIFO策略的缓存。示例代码如下:
/** 一个固定大小的FIFO替换策略的缓存 */
class FIFOCache<K, V> extends LinkedHashMap<K, V>{
private final int cacheSize;
public FIFOCache(int cacheSize){
this.cacheSize = cacheSize;
}
// 当Entry个数超过cacheSize时,删除最老的Entry
@Override
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return size() > cacheSize;
}
}
3.3 TreeMap
特点:
1.TreeMap
底层通过红黑树(Red-Black tree)实现,也就意味着containsKey(), get(), put(), remove()都有着log(n)的时间复杂度;
2.TreeMap实现了SortedMap接口,也就是说会按照key的大小顺序对Map中的元素进行排序,也可以传入比较器。
3.3 WeakHashMap*
特点: WeakHashMap 里的entry可能会被GC自动删除,即使程序员没有调用remove()或者clear()方法。更直观的说,当使用 WeakHashMap 时,即使没有显示的添加或删除任何元素,也可能发生如下情况:
1.调用两次size()方法返回不同的值;
2.两次调用isEmpty()方法,第一次返回false,第二次返回true;
3.两次调用containsKey()方法,第一次返回true,第二次返回false,尽管两次使用的是同一个key;
4.两次调用get()方法,第一次返回一个value,第二次返回null,尽管两次使用的是同一个对象。
适用场景: 适用于需要缓存的场景。在缓存场景下,由于内存是有限的,不能缓存所有对象;对象缓存命中可以提高系统效率,但缓存MISS也不会造成错误,因为可以通过计算重新得到。
造成该特点的原因: Java中内存是通过GC自动管理的,GC会在程序运行过程中自动判断哪些对象是可以被回收的,并在合适的时机进行内存释放。GC判断某个对象是否可被回收的依据是,是否有有效的引用指向该对象,这里的有效引用并不包括弱引用。而WeakHashMap中的对象就是弱引用,有可能被CG自动回收了,所以会发生这些奇怪情况。
Weak HashSet获取方式: java中没有对应的WeakHashSet对象,可以通过Collections.newSetFromMap(Map<E,Boolean> map)
将Map类包装为Set类。
// 将WeakHashMap包装成一个Set
Set<Object> weakHashSet = Collections.newSetFromMap(
new WeakHashMap<Object, Boolean>());
3.4 Map集合的遍历
第一种方式: 通过键找值的方式
1.使用Map集合中的keyset()
方法,把Map集合中的key取出来放在一个set集合中
2.遍历set集合,获取每一个key值(使用迭代器/增强for)
3.通过Map中的方法get (key)
找到对应的value
第二种方式: 使用Map的内部类Entry对象遍历
1.使用Map集合中的方法entrySet()
,把Map集合中的多个Entry对象取出来,存储到一个set集合中
2.遍历set集合,获取每一个Entry对象
3.使用Entry对象中的getKey()
和getValue()
方法获取键值
Map<String, Integer> map = new HashMap<String, Integer>();
map.put("黄晓明", 33);
map.put("胡歌", 28);
Set<Map.Entry<String, Integer>> entries = map.entrySet();
Iterator<Map.Entry<String, Integer>> iterator = entries.iterator();
while (iterator.hasNext()) {
Map.Entry<String, Integer> entry = iterator.next();
System.out.println("key:"+entry.getKey()+" value:"+entry.getValue());
}
三、泛型机制
1、泛型概述
泛型是一种未知的数据类型,可以当成一种变量,用来接收数据类型,创建对象的时候,就会确定泛型的数据类型。
泛型的意义:
1.适用于多种数据类型执行相同的代码【代码复用】;
2.类型在使用时指定,不需要强制类型转换(类型安全,编译器会检查类型)【类型约束】;
//它将提供类型的约束,提供编译前的检查
// list中只能放String, 不能放其它类型的元素
List<String> list = new ArrayList<String>();
2、泛型的使用
2.1 泛型类
一个简单的泛型类:
class Point<T>{ // 此处可以随便写标识符号,T是type的简称
private T var; // var的类型由T指定,即:由外部指定
public T getVar() {
return var;
}
public void setVar(T var) {
this.var = var;
}
public static void main(String args[]){
Point<String> p = new Point<String>() ; // 里面的var类型为String类型
p.setVar("it") ; // 设置字符串
System.out.println(p.getVar().length()) ; // 取得字符串的长度
}
}
多元泛性类:
class Notepad<K,V>{ // 此处可以随便写标识符号,T是type的简称
private K key;
private V value;
public K getKey() {
return key;
}
public void setKey(K key) {
this.key = key;
}
public V getValue() {
return value;
}
public void setValue(V value) {
this.value = value;
}
public static void main(String args[]){
Notepad<String,Integer> notepad = new Notepad<>();
notepad.setKey("小米");
notepad.setValue(1200);
System.out.print("姓名;" + notepad.getKey()) ; // 取得信息
System.out.print(",年龄;" + notepad.getValue()) ; // 取得信息
}
}
2.2 泛型接口
//定义一个泛型接口
interface Info<T>{
public T getVar() ;
}
//实现接口
class InfoImpl<T> implements Info<T>{
private T var;
@Override
public T getVar() {
return this.var;
}
public void setVar(T var) {
this.var = var;
}
public static void main(String[] args) {
InfoImpl<String> info = new InfoImpl<>();
info.setVar("hello");
System.out.println(info.getVar());
}
}
2.3 泛型方法
创建泛型方法:
调用泛型方法语法格式:
2.4 泛型上下限
当代码中存在隐式类型转换时:
class A{}
class B extends A {}
// 如下两个方法不会报错
public static void funA(A a) {
// ...
}
public static void funB(B b) {
funA(b);
// ...
}
// 如下funD方法会报错
public static void funC(List<A> listA) {
// ...
}
public static void funD(List<B> listB) {
//报错,因为存在隐式类型转换
funC(listB); // Unresolved compilation problem: The method doPrint(List<A>) in the type test is not applicable for the arguments (List<B>)
// ...
}
解决方法:加入了类型参数的上下边界机制<? extends A>
,编译器知道类型参数的范围,如果传入的实例类型B是在这个范围内的话允许转换,这时只要一次类型转换就可以了,运行时会把对象当做A的实例看待。
public static void funC(List<? extends A> listA) {
// ...
}
public static void funD(List<B> listB) {
funC(listB); // 编译器检查,在允许范围内
// ...
}
2.5 泛型通配符
<?>
代表任意的数据类型
适用场景: 定义一个方法遍历ArrayList集合,但是不确定ArrayList集合使用什么数据类型,则可以使用<?>来接收数据类型(即,同时接收多种类型)。ps:不能创建对象使用,只能作为方法的参数
通配符的使用(以上述)
interface Info<T>{
public T getVar() ;
}
class InfoImpl<T> implements Info<T>{
private T var;
@Override
public T getVar() {
return this.var;
}
public void setVar(T var) {
this.var = var;
}
public static void main(String[] args){
InfoImpl<?>[] list = new InfoImpl<?>[2];
InfoImpl<String> info1 = new InfoImpl<>();
info1.setVar("String");
InfoImpl<Integer> info2 = new InfoImpl<>();
info2.setVar(222);
list[0] = info1;
list[1] = info2;
trvalList(list);
}
public static void trvalList(InfoImpl<?>[] list){
for(int i =0;i<list.length;i++){
System.out.println(list[i].getVar());
}
}
}
泛型数组:
//声明一组泛型数组
List<String>[] list11 = new ArrayList<String>[10]; //编译错误,非法创建
List<String>[] list12 = new ArrayList<?>[10]; //编译错误,需要强转类型
List<String>[] list13 = (List<String>[]) new ArrayList<?>[10]; //OK,但是会有警告
List<?>[] list14 = new ArrayList<String>[10]; //编译错误,非法创建
List<?>[] list15 = new ArrayList<?>[10]; //OK
List<String>[] list6 = new ArrayList[10]; //OK,但是会有警告
合理的创建泛型数组:
public ArrayWithTypeToken(Class<T> type, int size) {
array = (T[]) Array.newInstance(type, size);
}
3、泛型使用小结
<?> 无限制通配符
<? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类
<? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类
& 可以用来多个限制 <T extends Staff & Passenger>
// 使用原则《Effictive Java》
// 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限
1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>;
2. 如果它表示一个 T 的消费者,就使用 < ? super T>;
3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。
4、深入理解泛型
4.1 伪泛型
说明: java中的泛型是一种“伪泛型”,由于泛型是java1.5引入,为了兼容之前的版本所以采用该策略。
含义: Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。
类型擦除的原则:
1.消除类型参数声明,即删除<>
及其包围的部分;
2.根据类型参数的上下界推断并替换所有的类型参数为原生态类型:无限制通配符或没有上下界限定则替换为Object;如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型;
3.为了保证类型安全,必要时插入强制类型转换代码;
4.自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。
类型擦除的过程:
无限制类型擦除:
有限制类型擦除:
<T extends Number>
和<? extends Number>
的类型参数被替换为Number
,<? super Number>
被替换为Object
擦除方法定义中的类型参数:
4.2 如何理解泛型的编译期检查
既然说类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢?
说明: Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。
例子:
下面的程序中,使用add方法添加一个整型,会直接报错,说明这就是在编译之前的检查。因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add("123");
list.add(123);//编译错误
}
4.3 桥接方法
类型擦除会造成多态的冲突,而JVM解决方法就是桥接方法。
例子——
有这样一个泛型类:
class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
创建一个子类:
class DateInter extends Pair<Date> {
@Override
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}
}
在这个子类中,我们设定父类的泛型类型为Pair<Date>
,那么父类里面的两个方法的参数都为Date
类型。然而经过泛型擦除后,父类的方法参数变为了Object
类型,这样,子类方法对父类方法根本就不会是重写,而是重载。为了解决这样的冲突,JVM采用桥接方法来解决。
我们用javap -c className的方式反编译下DateInter子类的字节码,结果如下:
class com.tao.test.DateInter extends com.tao.test.Pair<java.util.Date> {
com.tao.test.DateInter();
Code:
0: aload_0
1: invokespecial #8 // Method com/tao/test/Pair."<init>":()V
4: return
public void setValue(java.util.Date); //我们重写的setValue方法
Code:
0: aload_0
1: aload_1
2: invokespecial #16 // Method com/tao/test/Pair.setValue:(Ljava/lang/Object;)V
5: return
public java.util.Date getValue(); //我们重写的getValue方法
Code:
0: aload_0
1: invokespecial #23 // Method com/tao/test/Pair.getValue:()Ljava/lang/Object;
4: checkcast #26 // class java/util/Date
7: areturn
public java.lang.Object getValue(); //编译时由编译器生成的桥方法
Code:
0: aload_0
1: invokevirtual #28 // Method getValue:()Ljava/util/Date 去调用我们重写的getValue方法;
4: areturn
public void setValue(java.lang.Object); //编译时由编译器生成的桥方法
Code:
0: aload_0
1: aload_1
2: checkcast #26 // class java/util/Date
5: invokevirtual #30 // Method setValue:(Ljava/util/Date; 去调用我们重写的setValue方法)V
8: return
}
即自动创建Object
类型的桥方法,用来覆盖父类的方法,桥方法中调用子类重写的方法。
4.4 一些关于泛型的问答
问:如何理解基本类型不能作为泛型类型?
答:因为当类型擦除后,对象原始类型变为Object,但是Object类型不能存储int值,只能引用Integer的值。
问:如何理解泛型类型不能实例化?
答:这本质上是由于类型擦除决定的。在 Java 编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了。可以通过反射实现:
T test = new T(); // 错误!
//可以通过反射实例化泛型类型
static <T> T newTclass (Class < T > clazz) throws InstantiationException, IllegalAccessException {
T obj = clazz.newInstance();
return obj;
}
问:如何理解泛型类中的静态方法和静态变量?
答:泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数。因为泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。
//错误
public class Test2<T> {
public static T one; //编译错误
public static T show(T one){ //编译错误
return null;
}
}
public class Test2<T> {
//因为这是一个泛型方法,在泛型方法中使用的T是自己在方法中定义的 T,而不是泛型类中的T
public static <T> T show(T one){ //这是正确的
return null;
}
}
问:如何理解异常中使用泛型?
答:1.不能抛出也不能捕获泛型类的对象;2.不能再catch子句中使用泛型变量;3.但是在异常声明中可以使用类型变量,例如:
public static<T extends Throwable> void doWork(T t) throws T {
try{
...
} catch(Throwable realCause) {
t.initCause(realCause);
throw t;
}
}
问:既然类型被擦除了,那么如何获取泛型的参数类型呢?
答:可以通过反射(java.lang.reflect.Type)
获取泛型。
public class GenericType<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public static void main(String[] args) {
GenericType<String> genericType = new GenericType<String>() {};
Type superclass = genericType.getClass().getGenericSuperclass();
//getActualTypeArguments 返回确切的泛型参数, 如Map<String, Integer>返回[String, Integer]
Type type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
System.out.println(type);//class java.lang.String
}
}
四、注解机制
1、注解基础
注解是在JDK1.5之后引入的新特性,用于对代码进行说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解。作用有:
- 生成文档,通过代码里标识的元数据生成javadoc文档
- 编译检查,通过代码里标识的元数据让编译器在编译期间进行检查验证
- 编译时动态处理,编译时通过代码里标识的元数据动态处理,例如动态生成代码
- 运行时动态处理,运行时通过代码里标识的元数据动态处理。
2、Java自带的标准注解
@Override
标明重写某个方法@Deprecated
标明某个类或方法过时@SuppressWarnings
标明要忽略的警告
3、java元注解
元注解是用于定义注解的注解,包括@Retention
、@Target
、@Inherited
、@Documented
@Retention
: 标明注解被保留的阶段,包括源代码阶段(RetentionPolicy.SOURCE
)、编译阶段【默认】(RetentionPolicy.CLASS
)、运行时阶段(RetentionPolicy.RUNTIME
)@Target
: 标明注解的对象,包括方法、类、属性等,取值范围定义在ElementType
枚举中:
public enum ElementType {
TYPE, // 类、接口、枚举类
FIELD, // 成员变量(包括:枚举常量)
METHOD, // 成员方法
PARAMETER, // 方法参数
CONSTRUCTOR, // 构造方法
LOCAL_VARIABLE, // 局部变量
ANNOTATION_TYPE, // 注解类
PACKAGE, // 可用于修饰:包
TYPE_PARAMETER, // 类型参数,JDK 1.8 新增
TYPE_USE // 使用类型的任何地方,JDK 1.8 新增
}
@Inherited
: 被它修饰的Annotation将具有继承性,如果某个类使用了被@Inherited
修饰的Annotation
,则其子类将自动具有该注解@Documented
: 描述在使用 javadoc 工具为类生成帮助文档时是否要保留其注解信息@Repeatable
: 重复注解(jdk8),允许在同一申明类型(类,属性,或方法)的多次使用同一个注解。
//重复注解的例子:
@Repeatable(Authorities.class)
public @interface Authority {
String role();
}
public @interface Authorities {
Authority[] value();
}
public class RepeatAnnotationUseNewVersion {
@Authority(role="Admin")
@Authority(role="Manager")
public void doSomeThing(){ }
}
@Native
: 注解修饰成员变量,则表示这个变量可以被本地代码引用,常常被代码生成工具使用。
4、自定义注解
定义自己的注解:
package com.pdai.java.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyMethodAnnotation {
public String title() default "";
public String description() default "";
}
使用注解:
import java.io.FileNotFoundException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
public class TestMethodAnnotation {
@Override
@MyMethodAnnotation(title = "toStringMethod", description = "override toString method")
public String toString() {
return "Override toString method";
}
@Deprecated
@MyMethodAnnotation(title = "old static method", description = "deprecated old static method")
public static void oldMethod() {
System.out.println("old method, don't use it.");
}
@SuppressWarnings({"unchecked", "deprecation"})
@MyMethodAnnotation(title = "test method", description = "suppress warning static method")
public static void genericsTest() throws FileNotFoundException {
List l = new ArrayList();
l.add("abc");
oldMethod();
}
}
5、深入理解注解
注解支持继承吗?
注解是不支持继承的。不能使用关键字extends来继承某个@interface,但注解在编译后,编译器会自动继承java.lang.annotation.Annotation接口。另外,一个类使用@Inherited修饰的注解,该类的子类自动具有该注解。
注解@interface 是一个继承了Annotation接口的接口,里面每一个属性,其实就是接口的一个抽象方法。
(注解是框架的灵魂)
五、异常机制
1、异常的层次结构
-
Throwable
Throwable 是 Java 语言中所有错误与异常的超类。
Throwable 包含两个子类:Error(错误)和 Exception(异常),它们通常用于指示发生了异常情况。
Throwable 包含了其线程创建时线程执行堆栈的快照,它提供了printStackTrace()
等接口用于获取堆栈跟踪数据等信息。 -
Error(错误)
一般表示代码运行时 JVM 出现问题,此类错误发生时,JVM 将终止线程。 -
Exception(异常)
程序本身可以捕获并且可以处理的异常。Exception 这种异常又分为两类:运行时异常和编译时异常。
运行时异常: 编译器不会检查它,一般是由程序逻辑错误引起的
非运行时异常: 从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等。
2、异常的声明(throws)
若方法中存在检查异常,如果不对其捕获,那必须在方法头中显式声明该异常,以便于告知方法调用者此方法有异常,需要进行处理。
public static void method() throws IOException, FileNotFoundException{
//something statements
}
注意: 若是父类的方法没有声明异常,则子类继承方法后,也不能声明异常。
3、异常的抛出(throw)
如果代码可能会引发某种错误,可以创建一个合适的异常类实例并抛出它,这就是抛出异常。
public static double method(int value) {
if(value == 0) {
throw new ArithmeticException("参数不能为0"); //抛出一个运行时异常
}
return 5.0 / value;
}
说明: 大部分情况下都不需要手动抛出异常,因为Java的大部分方法要么已经处理异常,要么已声明异常,所以一般都是捕获异常或者再往上抛。抛出异常适用的场合在于,异常类型可能有多种,可以用统一的异常类型向外暴露,不需暴露太多内部异常细节。例如:
private static void readFile(String filePath) throws MyException {
try {
// code
} catch (IOException e) {
MyException ex = new MyException("read file failed.");
ex.initCause(e);
throw ex;
}
}
4、自定义异常
习惯上,定义一个异常类应包含两个构造函数,一个无参构造函数和一个带有详细描述信息的构造函数(Throwable 的 toString 方法会打印这些详细信息,调试时很有用), 比如上面用到的自定义MyException:
public class MyException extends Exception {
public MyException(){ }
public MyException(String msg){
super(msg);
}
// ...
}
5、异常的捕获
5.1 try-catch
在一个 try-catch 语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理,也可以捕获多种类型异常,用 | 隔开
private static void readFile(String filePath) {
try {
// code
} catch (FileNotFoundException | UnknownHostException e) {
// handle FileNotFoundException or UnknownHostException
} catch (IOException e){
// handle IOException
}
}
try-catch-finally
try {
//执行程序代码,可能会出现异常
} catch(Exception e) {
//捕获异常并处理
} finally {
//必执行的代码
}
执行顺序:
5.2 try-finally
try块中引起异常,异常代码之后的语句不再执行,直接执行finally语句。 try块没有引发异常,则执行完try块就执行finally语句。
5.3 try-with-resource
java1.7引入的语法糖,可以自动回收资源。
private static void tryWithResourceTest(){
try (Scanner scanner = new Scanner(new FileInputStream("c:/abc"),"UTF-8")){
// code
} catch (IOException e){
// handle exception
}
}
其中Scanner类:
public final class Scanner implements Iterator<String>, Closeable {
// ...
}
public interface Closeable extends AutoCloseable {
public void close() throws IOException;
}
6、使用异常的一些总结
- 优先捕获最具体的异常
- 不要捕获 Throwable 类
- 不要忽略异常,合理的做法是至少要记录异常的信息
- 包装异常时不要抛弃原始的异常
- 不要记录并抛出异常
- 不要使用异常控制程序的流程:异常非常耗时
- 不要在finally块中使用return
7、深入理解异常
一个简单的异常:
public static void simpleTryCatch() {
try {
testNPE();
} catch (Exception e) {
e.printStackTrace();
}
}
使用javap来分析这段代码:
//javap -c Main
public static void simpleTryCatch();
Code:
0: invokestatic #3 // Method testNPE:()V
3: goto 11
6: astore_0
7: aload_0
8: invokevirtual #5 // Method java/lang/Exception.printStackTrace:()V
11: return
Exception table:
from to target type
0 3 6 Class java/lang/Exception
当一个异常发生时:
- JVM会在当前出现异常的方法中,查找异常表,是否有合适的处理者来处理
- 如果当前方法异常表不为空,并且异常符合处理者的from和to节点,并且type也匹配,则JVM调用位于target的调用者来处理。
- 如果上一条未找到合理的处理者,则继续查找异常表中的剩余条目
- 如果当前方法的异常表无法处理,则向上查找(弹栈处理)刚刚调用该方法的调用处,并重复上面的操作。
- 如果所有的栈帧被弹出,仍然没有处理,则抛给当前的Thread,Thread则会终止。
- 如果当前Thread为最后一个非守护线程,且未处理异常,则会导致JVM终止运行。
六、反射机制
1、概述
反射就是把java类中的各种成分映射成一个个的Java对象。
例如:一个类有:成员变量、方法、构造方法、包等等信息,利用反射技术可以对一个类进行解剖,把个个组成部分映射成一个个对象。
2、Class类
1.Class类也是一个实实在在的类,存在于JDK的java.lang包中。
2.每个java类运行时都在JVM里表现为一个Class对象,可通过类名.class、类型.getClass()、Class.forName(“类名”)等方法获取class对象
3.数组同样也被映射为class 对象的一个类,所有具有相同元素类型和维数的数组都共享该 Class 对象。
4.基本类型boolean,byte,char,short,int,long,float,double和关键字void同样表现为 class 对象。
5.Class类只存私有构造函数,因此对应Class对象只能有JVM创建和加载
6.Class类的对象作用是运行时提供或获得某个对象的类型信息
3、java类加载机制
3.1 类的生命周期
类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。
其中,加载、验证、准备、初始化发生的顺序确定,通常是交叉混合进行的。解析发生的顺序不一定,这是为了支持java语言的运行时绑定。
3.2 类的加载
加载是类加载过程的第一个阶段,在加载阶段,虚拟机需要完成以下三件事情:
1.通过全限定类名获取二进制字节流
2.将字节流代表的静态存储结构转化为方法区的运行时数据结构
3.在java堆中创建一个java.lang.Class对象,作为访问方法区这些数据的入口。
3.3 连接
-
验证:确保被加载的类的正确性
这一阶段是为了保证字节流文件中的信息符合虚拟机的要求。主要有文件格式验证、元数据验证、字节码验证、符号引用验证。ps: 验证阶段是非常重要的,但不是必须的,在某些情况下那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间
-
准备:为类的静态变量分配内存,并将其初始化为默认值
-
解析:把类中的符号引用转换为直接引用
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。
3.4 初始化
初始化主要针对类变量。Java中类变量初始化有两种方式:
- 声明时指定类变量的初始值
- 使用静态代码块初始化类变量
JVM初始化的步骤:
- 假如这个类还没有被加载和连接,则程序先加载并连接该类
- 假如该类的直接父类还没有被初始化,则先初始化其直接父类
- 假如类中有初始化语句,则系统依次执行这些初始化语句
类初始化时机(只有出现以下情况时才会导致类的初始化)
- 通过new创建类的实例
- 访问某个类或者接口的静态变量,或者对该静态变量赋值
- 调用静态方法
- 反射
- 初始化某个类的子类,其父类也会被初始化
- Java虚拟机启动时被标明为启动类的类
3.5 卸载
在以下情况JVM将结束生命周期:
- 执行了System.exit()方法
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致Java虚拟机进程终止
4、类加载器
4.1 类加载器的层次
注意: 这里父类加载器并不是通过继承关系来实现的,而是采用组合实现的。
从开发人员角度类加载器大致划分为以下三类:
- 启动类加载器: Bootstrap ClassLoader,负责加载存放在JDK\jre\lib下或被-Xbootclasspath参数指定的路径中的类库。启动类加载器是无法被Java程序直接引用的。
- 扩展类加载器: Extension ClassLoader,负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
- 应用程序类加载器: Application ClassLoader,负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,默认的类加载器。
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println(loader);
System.out.println(loader.getParent());
System.out.println(loader.getParent().getParent());
}
}
结果如下:
sun.misc.Launcher$AppClassLoader@64fef26a
sun.misc.Launcher$ExtClassLoader@1ddd40f3
null //BootstrapLoader(引导类加载器)是用C语言实现的,找不到一个确定的返回父Loader的方式
4.2 类的加载方式
类加载有三种方式:
1、命令行启动应用时候由JVM初始化加载
2、通过Class.forName()方法动态加载
3、通过ClassLoader.loadClass()方法动态加载
//类加载的例子
public class loaderTest {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader loader = HelloWorld.class.getClassLoader();
System.out.println(loader);
//使用ClassLoader.loadClass()来加载类,不会执行初始化块
loader.loadClass("Test2");
//使用Class.forName()来加载类,默认会执行初始化块
// Class.forName("Test2");
//使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块
// Class.forName("Test2", false, loader);
}
}
public class Test2 {
static {
System.out.println("静态初始化块执行了!");
}
}
Class.forName()
和ClassLoader.loadClass()
区别:
1.Class.forName()
: 将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块; 2.ClassLoader.loadClass()
: 只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance()
才会去执行static块;
3.Class.forName(name, initialize, loader)
带参函数也可控制是否加载static块。并且只有调用了newInstance()
方法采用调用构造函数,创建类的对象。
4.3 JVM的类加载机制
java采用双亲委派机制进行类的加载。过程如下:
- 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
- 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
- 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
- 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。
双亲委派机制的优点: 防止内存中出现多份同样的字节码;保证Java程序安全稳定运行
4.4 自定义类加载器
通常情况下,我们都是直接使用系统类加载器,为保证安全性,这些字节码经过了加密处理,这时系统类加载器就无法对其进行加载,这样则需要自定义类加载器来实现。只需要重写 findClass 方法即可。一般不要重写loadClass方法,因为这样容易破坏双亲委托模式。
双亲委托机制的类加载器代码:
public Class<?> loadClass(String name)throws ClassNotFoundException {
return loadClass(name, false);
}
protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
// 首先判断该类型是否已经被加载
Class c = findLoadedClass(name);
if (c == null) {
//如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
try {
if (parent != null) {
//如果存在父类加载器,就委派给父类加载器加载
c = parent.loadClass(name, false);
} else {
//如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
重写findClass方法:
import java.io.*;
public class MyClassLoader extends ClassLoader {
private String root;
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
//此处可以有解密文件的逻辑
private byte[] loadClassData(String className) {
String fileName = root + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
try {
InputStream ins = new FileInputStream(fileName);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length = 0;
while ((length = ins.read(buffer)) != -1) {
baos.write(buffer, 0, length);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public String getRoot() {
return root;
}
public void setRoot(String root) {
this.root = root;
}
public static void main(String[] args) {
MyClassLoader classLoader = new MyClassLoader();
classLoader.setRoot("D:\\temp");
Class<?> testClass = null;
try {
testClass = classLoader.loadClass("com.pdai.jvm.classloader.Test2");
Object object = testClass.newInstance();
System.out.println(object.getClass().getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
七、SPI机制
1、概述
SPI(Service Provider Interface),是JDK内置的一种 服务提供发现机制,可以用来启用框架扩展和替换组件。主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是解耦。
具体操作: 当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/
目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/
中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。JDK中查找服务的实现的工具类是:java.util.ServiceLoader
2、SPI机制的简单实例
//定义一个接口
public interface Search {
public List<String> searchDoc(String keyword);
}
//文件搜索实现
public class FileSearch implements Search{
@Override
public List<String> searchDoc(String keyword) {
System.out.println("文件搜索 "+keyword);
return null;
}
}
//数据库搜索实现
public class DatabaseSearch implements Search{
@Override
public List<String> searchDoc(String keyword) {
System.out.println("数据搜索 "+keyword);
return null;
}
}
接下来可以在resources下新建META-INF/services/
目录,然后新建接口全限定名的文件:com.myproject.Search
,里面加上我们需要用到的实现类:
com.myproject.Search
测试:
public class TestCase {
public static void main(String[] args) {
ServiceLoader<Search> s = ServiceLoader.load(Search.class);
Iterator<Search> iterator = s.iterator();
while (iterator.hasNext()) {
Search search = iterator.next();
search.searchDoc("hello world");
}
}
}
这就是spi的思想,接口的实现由provider实现,provider只用在提交的jar包里的META-INF/services
下根据平台定义的接口新建文件,并添加进相应的实现类内容就好。
3、深入理解SPI
SPI机制通常使用流程:
1.定义标准,就是定义接口,比如接口java.sql.Driver
2.厂商或者框架开发者开发具体的实现:在META-INF/services
目录下定义一个名字为接口全限定名的文件,比如java.sql.Driver
文件,文件内容是具体的实现名字,比如me.cxis.sql.MyDriver
。
3.程序员使用,引用具体厂商的jar包来实现我们的功能:
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
//获取迭代器
Iterator<Driver> driversIterator = loadedDrivers.iterator();
//遍历
while(driversIterator.hasNext()) {
driversIterator.next();
//可以做具体的业务逻辑
}
SPI和API的区别:
1.SPI-“接口”位于“调用方”所在的“包”中
-概念上更依赖调用方。
-组织上位于调用方所在的包中。
-实现位于独立的包中。
-常见的例子是:插件模式的插件。
2.API – “接口”位于“实现方”所在的“包”中
-概念上更接近实现方。
-组织上位于实现方所在的包中。
-实现和接口在一个包中。
4、SPI机制的缺陷
- 不能按需加载,需要遍历所有的实现,并实例化,然后在循环中才能找到我们需要的实现。如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
- 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
- 多个并发多线程使用 ServiceLoader 类的实例是不安全的