JavaSE
基础部分数值型:
byte
short
int
long
float
double
字符型:char
布尔型:boolean
class
类
interface
接口
数组
switch
是否能作用在byte
上,是否能作用long
上,是否能作用在String
上?在Java5
以前,Switch(expr)
中,expr
只能是byte
shrot
char
int
,从java5
开始,java
中引入了枚举类型,expr
也可以是enum
类型,从java7
开始,expr
还可以是字符串String
,但是是长整型long
在目前的版本中都是不可以的
final
关键字1.final
修饰类时,该类是不能被继承的
String
StringBuffer
[线程安全] StringBuilder
[线程不安全] 八大包装类
2.final
修饰方法时,该方法不能被重写,但是可以被继承,可以被重载
Object
中getClass
方法就是使用了final
关键字修饰的,比如:当我们希望某个类中的某个方法,不被重写时,就可以使用final
关键字修饰的3.final
可以修饰变量,1》如果是基本数据类型的变量,则数值一旦在初始化后就不能被修改了,一般情况下我都会使用static
和final
来修饰基本数据类型变量,达到常量的效果,2》如果 是引用类型变量,则在对其初始化之后便不能再指向另一个对象,但是是指向的内容是可以被修饰的【例如:一个学生对象是final
修饰的,所以它就是不能重新指定另一个学生对象,但是学生对象中有name
属性,指,但是学生对象中name
属性的值是可以被修改的【前提这个name
属性是没有使用final
修饰的】】
String
类中的value
就是使用value
修改的private final char value[];
final
修饰变量可以在初始化直接进行赋值,也可以在代码块中赋值,也在构造器中赋值[String类的value属性就是在构造器进行赋值的
],如果这个属性同时也使用static
修饰,那么这个属性赋值只能在静态代码中或定义时直接赋值,final
在局部代码块修饰变量的话必须可以在声明时直接赋值,也在后面的代码块中赋值【这个可以省略不答】4.当一个类使用final
了关键字修饰,就没有必须又在方法中使用final
关键字修饰了
final
finally
finalize
区别final
可以修饰类,变量,方法,修饰类表示该类不能被继承,修饰方法表示该方法不能被重写,修饰变量表示该变量是一个常量不能被重新赋值finally
一般作用在try-catch
代码块中,在处理异常处理的时候,通常我们将一定要执行的代码放到finally
代码块中,表示不管是否出现异常,该代码都会被执行,一般用来关闭资源finalize
是一个方法,属于Object
类中一个方法,而Object
类是所有类的基类,该方法一般由垃圾回收器来调用,当我们调用system.ge()
方法的时候,由垃圾回收器调用finalize()
,回收垃圾,一个对象是否可回收最后判断this
关键字this
关键字代表是该类对象的引用,就是指向该对象本身,this
是每个对象都有的并且是隐藏的【谁调用代表谁】
使用this
注意事项和细节(必须掌握,记住)
this
关键字可以用来访问本类的属性,方法,构造器this
用于区分当前类的属性(全局变量)和局部变量this.方法名(参数列表)
this(参数列表);
注意是只能在构造器中使用,并且要放在构造器的第一条语句,不能在普通方法中使用(注意是在构造器中使用必须放在第一条语句,)this
不能在类定义的外部使用,只能在类定义的方法中使用(意思就是只能在本类中使用)Super
关键字super
关键字代表是父类引用 ,用于访问父类的属性,方法,构造器
基本语法 记住
1.访问父类的属性,但不能访问父类的private属性 super.属性名;
2.访问父类的方法,不能访问父类的private
方法 super.方法名(参数列表);
3.访问父类的构造器 super(参数列表);
只能放在构造器的第一语句,只能出现一句!
super
关键字细节(记住)
1.调用父类的构造器的好处(分工明确,父类属性由父类初始化,子类的属性由子类初始化)
2.当子类中有和父类中的成员(属性和方法)重名时,为了访问父类的成员,必须通过super
。如果没有重名,使用super
,this
直接访问是一样的效果
3.super
的访问不限于直接父类,如果爷爷类和本类中有同名的成员,也可以使用super
去访问爷爷类的成员;如果多个基类(上级类)中都有同名的成员,使用super
访问遵循就近原则。
A->B->C->...->Object
,当然也需要遵守访问权限的相关规则
super
和this
比区别记下
区别点 | this | super |
---|---|---|
访问属性 | 访问本类中属性,如果本类没有此属性则从父类中继续查找 | 访问父类中的属性 |
调用方法 | 访问本类中的方法,如果本类没有此方法则从父类继续查找 | 直接访问父类中的方法 |
调用构造器 | 调用本类构造器,必须放在构造器的首行 | 调用父类构造器,必须放在子类构造器的首行 |
特殊点 | 表示当前对象 | 子类中访问父类对象 |
getMessage()
返回异常发生时详细信息toString()
返回异常发生时简要描述getLocalizedMessage()
返回异常对象本地化信息Java
异常接口参考答案:
Throwable
是异常的顶层父类,代表所有的非正常情况。它有两个直接子类,分别是Error
Exception
Error
是错误,一般是指系统崩溃,如:StackOverflowError
【栈溢出】和内存不足OOM
(out of memory
),Error
是严重错误,程序会崩溃,并且不能使用try-catch
块来捕获Error
对象,也不需要使用throws
子句声明该方法可能抛出Error
及其任何子类
Exception
是异常,它被分为两类,分别是checked
异常【编译时异常】和Runtime
异常【运行时异常】,所有的RuntimeException
类及其子类的实例被称为Runtime
异常,不是RuntimeException
类及其子类的异常实例则被称为checked
异常【编译时异常】,在java
中编译时异常,是必须处理的,否则无法通过编译。而Runtime
异常则更加灵活,Runtime
异常无须显式声明抛出或使用try-catch
来捕获
Java
异常处理机制关于异常处理:在Java
中有两种方式:方式一是:try-catch-finally
方式二是:throws
抛出
在Java
中,处理异常的语句由try-catch-finally
三部组分组成,其中,try
块用于处理业务的,catch
块用于捕获并处理异常的,而finally
是用于回收资源,并且不管是否发生异常finally
块一定被执行
使用throws
抛出异常,当程序出现错误时,系统自动抛出异常。一般在java
中判断某项错误的条件成立时,可以使用throw
关键字向外抛出异常,让调用者来处理该异常,可以一直向上抛出异常让JVM
处理【JVM
处理方式一般只是打印异常信息】,使用方式是:如果当前方法不知道该如何处理这个异常时就可以将异常抛出,让调用者来处理
finally
是无条件执行的吗?不管在try
中的代码是否现出异常,也不管哪个catch
块被执行,甚至在try
块或catch
块中执行了finally
块总会被执行
注意事项:
如果在try
或catch
块中使用System.exit()
方法,来退出java
虚拟机,则finally
块将不会被执行,我们一般不会退出java
虚拟机,因为我们想要程序一直正常的执行下去
参考答案
在通常情况下,不要在finally块中使用return、throw等导致方法终止的语句,一旦在finally块中使用了return、throw语句,将会导致try块、catch块中的return、throw语句失效。
详细解析
当Java程序执行try块、catch块时遇到了return或throw语句,这两个语句都会导致该方法立即结束,但是系统执行这两个语句并不会结束该方法,而是去寻找该异常处理流程中是否包含finally块,如果没有finally块,程序立即执行return或throw语句,方法终止;如果有finally块,系统立即开始执行finally块。只有当finally块执行完成后,系统才会再次跳回来执行try块、catch块里的return或throw语句;如果finally块里也使用了return或throw等导致方法终止的语句,finally块已经终止了方法,系统将不会跳回去执行try块、catch块里的任何代码。
NullPointerException
:空指针异常ArithmeicException
:数学运算异常ArrayIndexOutOfBoundsException
:数组下标越界异常ClassCastException
:类型转换异常NumberFormatException
:数字格式不正确异常介绍:编译异常是指在编译期间,就必须处理的异常,否则代码不能通过编译
常见的编译异常:
SQLException
:操作数据库时,查询表可能发生异常
IOException
:操作文件时,发生的异常
FileNotFoundException
:当操作一个不存在的文件时,发生异常
ClassNotFoundException
:加载类,而该类不存在时,异常
EOFException
:操作文件,到文件未尾,发生异常
ILLegaIArguementException
:参数异常
在Java
语言面向对象语言,其设计理念是一切皆对象,但是8种基本数据类型却出现了例外,它们不具备对象的特性,所以Java
每一个基本数据类型都定义了一个对应的引用类型,就是包装类
演示:包装类和基本数据类型的相互转换,这里以int
和 Integer
演示:
jdk5
前的手动装箱和拆箱方式,**装箱:**基本类型 --》包装类型,反之,拆箱jdk5
以后(包含jdk5
)的自动装箱和拆箱方式valueOf
方法,比如Integer.valueOf()
intValue
方法,比如对象名.intValue()
valueOf源码:
public static Integer valueOf(int i) {
//low:范围是:-128
//high:范围是:127
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
题目:
示例1:引用数据类型
Integer i1 = new Integer(127);
Integer i2 = new Integer(127);
System.out.println(i1==i2);//false
示例2:引用数据类型
Integer i3 = new Integer(128);
Integer i4 = new Integer(128);
System.out.println(i3==i4);//false
示例3:引用数据类型:底层调用了 Integer.valueOf();
Integer i5 = 127;
Integer i6 = 127;
System.out.println(i5==i6);//true
示例4:引用数据类型
Integer i7 = 128;
Integer i8 = 128;
System.out.println(i7==i8);//false
示例5:引用数据类型
Integer i9 = 127;
Integer i10 = new Integer(127);
System.out.println(i9==i10);//false
示例6:一个基本数据类型一个引用数据类型 == 号比较就是值
Integer i11 = 127;
int i12 = 127;
System.out.ptintln(i11==i12);//true
示例7:一个基本数据类型一个引用数据类型 == 号比较就是值
Integer i13 = 128;
int i14 = 128;
System.out.println(i13==14);//true
只要是基本数据类型,"=="号判断是值是否相等
只要是引用数据类型,"=="号判断是地址值是否相等
如果一个是基本数据类型,另一个是引用数据类型,则判断还是值是否相等
String
String
对象用于保存字符串,也就是一组字符序列String
是一个final
类,代表不可变的字符序列unicode
字符编码,一个字符【不区分字母还是汉字】占两个字节String
类中有一个非常重要的属性是private final char value[];
的value
指向的地址是不可以改变的 ,而的存在的单个字符是可以改变的 String
类中的value
值是存在常量池中地址的,就是说value
值是用于维护常量池的地址,是不能改变的,改变的只能是常量池的内容,如用new
的方式创建对象【才有用value属性维护常量池的地址】String
对象的两种方式方式一:直接赋值 String s = "hsp";
方式二:调用构造器String s = new String("hsp");
两种创建String
对象的区别
1.方式一:先从常量池查看是否有"hsp"
数据空间,如果有,直接指向;如果没有重新创建,然后指向。s(对象引用名)
最终指向的是常量池的空间地址
2.方式二:先在堆中创建空间,里面维护了一个value
属性值【是用于存在常量的空间地址的】,指向常量池的hsp
空间。如果常量池没有hsp
,重新创建,如果有,直接通过value
指向。最终指向的是堆中的空间地址
重要规则:
String c = “ab”+“cd”;常量相加,看的是常量池
String c = a + b;变量相加,是在堆中【一个变量一个常量相加,也是堆中】
题1:
String a = "hello" + "abc";
创建了几个对象?只有1对象。
因为是: String a = "hello" + "abc";//底层优化成 String a = "helloabc";
分析是:
1.底层编译器做一个优化,判断创建的常量池对象,是否有引用指向
2.String a = "hello"+"abc"; => String a = "helloabc";
题2
String a = "hello";//创建了对象a
String b = "abc";//创建了b对象
String c = a + b;//创建了几个对象?画出内存图?
关键就是分析String c = a + b;到底是如何执行的
一共有3对象,如图。
总结:底层是:1.StringBuilder sb = new StringBuilder();
2.sb.append(a);
3.sb.append(b);
4.sb是在堆中,并且append是在原来字符串的基础上追加的
5.再调用StringBuilder类中的toString方法
【所以指向的地址是指向堆中的,堆有一个value维护着常量池的属性】
重要规则:
String c = "ab"+"cd";常量相加,看的是常量池
String c = a + b;变量相加,是在堆中【一个变量一个常量相加,也是堆中】
String
常用方法
equals
equalslgnoreCase
length
indexOf
lastIndexOf
substring
trim
charAt
:获取某个索引处字符,注意是不能使用str[index]
的方式,必须使用charAt
获取单个字符toUpperCase
toLowerCase
replace:替换
split:分割
concat:拼接字符串
compareTo:比较两个字符串
toCharArray:转换字符数组
format:格式字符串
StringBuffer
和StringBuilder
StringBuffer
和StringBuilder
的区别:
StringBuffer
和StringBuilder
都代表可变的字符串对象,它们共同的父类是AbstractStringBuilder
,并且两个类构造器和方法也是相同的,只是StringBuffer
是线程安全的,而StringBuilder
是线程不安全的,效率更高
StringBuffer
和StringBuilder
是可变的字符串对象,因为是:在父类中 AbstractStringBuffer有属性 char[] value,不是final 该 value 数组存放在字符串内容,引出存在在堆中的
StringBuffer
和StringBuilder
常用方法:append
delete
replace
indexOf
insert
length
new
和""
推荐使用哪种方式?先看hello
和new String("hello")
的区别:
java
直接使用hello
字符串直接量时,jvm
将使用常量池管理这个字符串new String("hello")
时,jvm
先使用常量池来管理hello
直接量,再调用String
类的构造器创建一个新的String
对象,新创建的String
对象保存在堆内存中显然,采用new
的方式会多创建一个对象,会占用更多的内存,所以一般建议使用直接量方式创建字符串
String
和 StringBuffer
和 StringBuilder
1.StringBuilder
和StringBuffer
非常类似,均代表可变的字符序列,而且方法也一样的
2.String
:是不可变字符序列,效率低,但是复用率高
3.StringBuffer
:可变字符序列,效率较高【增删】,线程安全,看源码
4.StringBuilder
:可变字符序列,效率最高,线程不安全
5.String
使用注意说明
String s = "a";
//创建了一个字符串s += "b";
//实际上原来的a
字符串已经丢弃了,现在又产生了一个字符串s+b
【也就是"ab"
】。如果多次执行这些改变串内容的操作,会导致大量副本字符串对象存留在内存中,降低效率。如果这样的操作放到循环中,会极大影响程序的性能 ==》结论:如果我们对String
做大量修改,不要使用String
String
和 StringBuffer
和 StringBuilder
的选择StringBuffer
【多线程情况下】或 StringBuilder
【单线程情况下】StringBuilder
StringBuffer
String
,比如配置文件等String
StringBuffer
StringBuilder
区别:
String
底层是使用final
修饰的char[]
代表是不可变的字符序列,只要每次操作String
字符串,则每次都会生成新的对象,消耗内存
StringBuffer
和StringBuilder
都代表可变的字符串对象,它们共同的父类是AbstractStringBuilder
,并且两个类构造器和方法也是相同的,只是StringBuffer
是线程安全的,而StringBuilder
是线程不安全的,效率更高
从效率上:StringBuilder
大于StringBuffer
大于String
Math
工具类ceil
floor
round
sqrt
random
Arrays
类Arrays
类常见方法案例
Arrays
里面包含了一系列静态方法,用于管理或操作数组【比如排序和搜索】
1.toString 返回数组的字符串形式
Arrays.toString(arr);
2.sort 排序【自然排序和定制排序】[重点]
3.binarySearch 通过二分搜索法进行查找,要求必须排好序
int index = Arrays.binarySearch(arr,3);
4.copyOf 数组元素的复制
Integer newArr = Arrays.copyOf(arr,arr.length);
5.fill数组元素的填充
Integer[] num = new Integer[]{9,6,3};
Arrays.fill(num,8);//将数组num中的所有元素都替换成8
6.equals 比较两个数组元素内容是否完全一致
boolean equals = Arrays.equals(arr,arr2);//如果数组arr和arr2完全一样返回true,否则返回false
7.asList 将一组值,转换成List集合
List<Integer> asList = Arrays.asList(2,3,43,5,6,78,168);
System.out.println("asList="+asList);
System
类System
类常见方法和案例
1.exit
: 退出当前程序
2.arraycopy
:复制数组元素,比较适合底层调用,一般使用Arrays.copyOf
完成复制数组
int[] scr = {1,2,3};
int[] dest = new int[3];
System.arraycopy(src,0,dest,0,3);
3.currentTimeMillens
:返回当前时间距离1970-1-1
的毫秒数
4.gc
:运行垃圾回收机制System.gc();
JavaSE
补充部分void
)new
操作被系统调用,并且对于一个对象而言,只会被调用一次,所以说构造器是系统调用的super
关键字来显式调用父类构造器,完成对父类的初始化,默认情况下调用是父类无参构造器,当父类没有无参构造器时,子类必须使用super
关键字指定调用父类那个构造器完成父类初始化多态就是代码重用的一种机制,表示当同一个对象操作在不同的对象的时候,会产生不同的结果
1)重载
重载是指同一个对象中有多个同名的方法时,但是这些方法参数不同,在通过不同的参数调用不同的方法,就是可以体现方法的多态性
2)重写
子类可以重写父类的方法,可以看出同样的方法会在父类与子类中有着不同的表现形式,比如:父类的引用可以指向子类实例对象,同时接口引用也可以指向子类实例对象,在程序运行时运行期才确定调用是哪个方法【这种该用方法的方式就是动态绑定机制】,就是可以体现重写实现多态
重载:
重写:
方法重载overLoad
和方法重写override
名称 | 发生范围 | 方法名 | 参数列表 | 返回类型 | 修饰符 |
---|---|---|---|---|---|
重载overLoad | 本类 | 必须相同 | 类型,个数或者顺序至少有一个不同 | 无要求 | 无要求 |
重写override | 父子类 | 必须相同 | 必须相同 | 子类重写的方法,返回的类型和父类返回的类型一样,或者是其子类 | 子类方法不能缩小父类方法的访问范围 |
abstract
抽象和interface
接口相同点:
.Class
文件抽象类:
抽象类不一定要包含有abstract
方法,也就是说,抽象类可以没有abstract
方法,并且抽象方法可以有构造器
一旦类中包含有abstract
方法,则这个类必须声明成abstract
abstract
只能修饰类和方法,不能修饰属性
抽象类可以有任意成员【因为抽象类本质还是类,只是多了抽象方法】
抽象方法不能有方法体
如果一个类继承了抽象类,要么将所有抽象方法都实现,要么将自己声明为abstract
类
抽象类是单继承关系
接口:
jdk7
前接口里的所有方法都没有方法体,即都是抽象方法,并且变量必须是public static final
修饰的,方法都是public abstract
修饰也是默认的jdk8
以后可以有静态方法,默认方法default
修饰的,也就是说接口中可以有方法具体实现jdb8.0
后有默认方法,但是可以在默认方法中定义局部变量hashCode
和equals
及==
关系hashCode
用于获取哈希码(散列码),equals
是用于比较两个对象是否相等,
==
运算符
equals
方法
Object
中的equals
方法是以==
来实现的,比较两个对象内存地址是否相同,往往我们都进行重写,用于判断两个对象的内容是否相同,如果相同则认为对象相等,否则不相等为什么equals
方法和hashCode
方法一起重写?
原因是对于hashCode
相同对象,才会进一步比较是否equals
来判断他们是否相等,这是实现快速查找的关键
步骤克隆方法的步骤:
第一步:实现Cloneable
接口
第二步:重写clone
方法
浅拷贝:
被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象,就是说:浅拷贝仅仅复制了所有考虑的对象,而不是复制它所引用的对象
深拷贝:
被复制对象的所有变量都含有与原来的对象相同的值,而那些引用其他对象的变量将指向被复制边的新对象,而不再是原有的那些被引用 的对象,就是说:深拷贝把要复制的对象所引用的对象都复制了一遍
抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的 细节是什么。
继承:继承是从已有类得到继承信息创建新类的过程。提供继承信息的类 被称为父类(超类、基类);得到继承信息的类被称为子类(派生类)。继承让 变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要 手段。
封装:通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问 只能通过已定义的接口。面向对象的本质就是将现实世界描绘成一系列完全自治、封闭的对象。我们在类中编写的方法就是对实现细节的一种封装;我们编写 一个类就是对数据和数据操作的封装。可以说,封装就是隐藏一切可隐藏的东西, 只向外界提供最简单的编程接口(可以想想普通洗衣机和全自动洗衣机的差别, 明显全自动洗衣机封装更好因此操作起来更简单;我们现在使用的智能手机也是 封装得足够好的,因为几个按键就搞定了所有的事情)。
多态性:多态性是指允许不同子类型的对象对同一消息作出不同的响应。 简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。多态性分 为编译时的多态性和运行时的多态性。如果将对象的方法视为对象向外界提供的 服务,那么运行时的多态性可以解释为:当 A 系统访问 B 系统提供的服务时,B 系统有多种提供服务的方式,但一切对 A 系统来说都是透明的(就像电动剃须 刀是 A 系统,它的供电系统是 B 系统,B 系统可以使用电池供电或者用交流电, 甚至还有可能是太阳能,A 系统只会通过 B 类对象调用供电的方法,但并不知道 供电系统的底层实现是什么,究竟通过何种方式获得了动力)。方法重载 (overload)实现的是编译时的多态性(也称为前绑定),而方法重写(override) 实现的是运行时的多态性(也称为后绑定)。运行时的多态是面向对象最精髓的 东西,要实现多态需要做两件事:1). 方法重写(子类继承父类并重写父类中已 有的或抽象的方法);2). 对象造型(用父类型引用引用子类型对象,这样同样 的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为。
答: 数组没有 length()方法,有 length 的属性。String 有 length()方法。JavaScript 中,获得字符串的长度是通过 length 属性得到的,这一点容易和 Java 混淆。
1.泛型又称为参数化类型,是jdk5
出现的新特性,解决数据类型的安全性问题
2.在类声明或实例化时只要指定好需要的具体的类型即可
3.java
泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException
异常,同时,代码更加简洁,健壮
4.泛型的作用是:可以在类声明时通过一个标识表示类中某个属性的类型,或者是某个方法的返回国值的类型,或者是参数类型【重要点】
泛型是作用范围是:
1.标识类中某个属性
2.某个方法返回值类型
3.参数类型
特别注意是:E
类型在编译时就已经确定了,才能起到检查数据类型的作用
package collection_.collectionP.generic_;
/**
* @author: 海康
* @version: 1.0
*/
public class Generic03 {
public static void main(String[] args) {
// 注意是: E 此类型是在编译期间就确定了,所以才能起到检查数据安全性
Person<Integer> integerPerson = new Person<>(100);
integerPerson.show();
/**
* 加入泛型代码相当于下面
* class Person {// E表示是标识某个属性的类型,即在编译期间就确定了,所以才能起到检查数据安全性
*
* Integer e;// E 此类型是在编译期间就确定了,所以才能起到检查数据安全性
*
* public Person(Integer e){ // 泛型可以作为一个参数
* this.e = e;
* }
*
* public Integer f(){
* return e;// 泛型可以作为一个返回值类型
* }
*
* public void show() {
* System.out.println(e.getClass());//查看e运行类型
* }
* }
*/
}
}
/**
* 泛型的作用是:可以在类声明时通过一个标识表示某个属性的类型,
* 或者是某个方法返回值的类型,或者是参数类型
* @param
*/
class Person<E> {// E表示是标识某个属性的类型,即在编译期间就确定了,所以才能起到检查数据安全性
E e;// E 此类型是在编译期间就确定了,所以才能起到检查数据安全性
public Person(E e){ // 泛型可以作为一个参数
this.e = e;
}
public E f(){
return e;// 泛型可以作为一个返回值类型
}
public void show() {
System.out.println(e.getClass());//查看e运行类型
}
}
泛型的好处:
面试回答:
jdk5
出现的新特性,解决向下强转的操作,达到解决数据类型的安全性问题Object
类型基本语法:
class 类名<T,R,...>{// 表示可以有多个泛型
成员
}
注意细节:
1.普通成员可以使用泛型【属性,方法,返回值类型】
2.使用泛型的数组,不能初始化【就是不能直接new
,因为不能确定数据类型,无法分配空间,可以声明不能new
】
3.静态方法和静态属性中不能使用类的泛型【静态方法和静态属性是随着类加载而加载的】
4.泛型类的类型,是在创建对象时确定的【因为创建对象时,需要指定确定类型】
5.如果在创建对象时,没有指定类型,默认是Object
基本语法:
interface 接口<T,R,...>{
成员
}
注意细节:
1.接口中,静态成员【静态成员方法和静态成员属性,但是接口中的属性都是静态的所以属性不能使用泛型】不能使用泛型【和泛型类规定一样】
2.泛型接口的类型,在继承接口或实现接口时确定
3.没有指定类型,默认为Object
必须理解下面代码:
package collection_.collectionP.generic_;
/**
* @author: 海康
* @version: 1.0
*/
public class Generic05 {
public static void main(String[] args) {
}
}
interface IUsb<T,R> {
//普通成员方法中,可以使用泛型
// T num ; 错误的,因为在接口中的属性不能使用泛型
// 普通方法中使用泛型
T getT();
default R method(){//默认方法使用泛型
return null;
}
}
// 泛型接口的类型,在继承接口或实现接口时确定
// 在继承接口时,确定泛型
// String会自己替换T Integer会自己替换R
interface myInt extends IUsb<String,Integer>{}
class myClass implements myInt{
@Override
public String getT() {
return null;
}
}
// 实现接口类指定泛型
class A implements IUsb<String ,Integer>{
@Override
public String getT() {
return null;
}
}
// 如果没有指定泛型默认是Object
class B implements IUsb{
@Override
public Object getT() {
return null;
}
}
在调用泛型方法时,传入的参数,编译器就可以确定类型
基本语法:
修饰符<T,R...>返回类型 方法名(参数列表){}
注意细节:
1.泛型方法中,可以定义在普通类中,也可以定义在泛型类中
2.当泛型方法被调用时,类型会确定
3.一定要理解下面的细节
如果在泛型类定义了泛型方法,一般泛型类的泛型标识符是和泛型方法的泛型标识符是不一样的【也可以一样】
一般在泛型方法中使用的参数要和泛型方法标识一致
public void eat(T t){} 修饰符后没有泛型占位符<T,R>,说明eat方法不是泛型方法,只是使用了泛型
非常注意是:一般泛型方法要传入参数,而传入的参数一般是泛型方法的标识 ,如果是在泛型类中定义的泛型方法也可以传入泛型类的标识【但是一般会的都是泛型方法的标识 】
一定要理解下面的代码
package collection_.collectionP.generic_;
/**
* @author: 海康
* @version: 1.0
*/
public class CustomMethodGeneric {
public static void main(String[] args) {
}
}
// 泛型方法可以定义在普通类中,也可以定义在泛型类中
class myGeneric {
// 定义泛型方法【定义在普通类中】
public <T,R> void fi(T t,R r){}//一般泛型方法的泛型和传入的参数的泛型一致
// 【就是 和 fi(T t,R r)】
}
// 在泛型类中定义泛型方法
class MG<T,R>{
//定义泛型方法
public <K,V> void getKV(K k,V v){//一般泛型方法的标识和泛型类的标识是不一致的
// 一般使用的泛型参数就泛型方法标识 ,也可以使用泛型类的标识
}
public <K,V> void getKV2(T t,V v){//一般泛型方法的标识和泛型类的标识是不一致的
// 一般使用的泛型参数就泛型方法标识 ,也可以使用泛型类的标识
}
// 注意下面的方法不是泛型方法,只是使用了泛型
public void get(T t){}
}
package com.hspedu.customgeneric;
import java.util.ArrayList;
/**
* 泛型方法的使用
*/
@SuppressWarnings({"all"})
public class CustomMethodGeneric {
public static void main(String[] args) {
Car car = new Car();
car.fly("宝马", 100);//当调用方法时,传入参数,编译器,就会确定类型
System.out.println("=======");
car.fly(300, 100.1);//当调用方法时,传入参数,编译器,就会确定类型
//测试
//T->String, R-> ArrayList
Fish<String, ArrayList> fish = new Fish<>();
fish.hello(new ArrayList(), 11.3f);
}
}
//泛型方法,可以定义在普通类中, 也可以定义在泛型类中
class Car {//普通类
public void run() {//普通方法
}
//说明 泛型方法
//1. 就是泛型
//2. 是提供给 fly使用的
public <T, R> void fly(T t, R r) {//泛型方法
System.out.println(t.getClass());//String
System.out.println(r.getClass());//Integer
}
}
class Fish<T, R> {//泛型类
public void run() {//普通方法
}
public<U,M> void eat(U u, M m) {//泛型方法
}
//说明
//1. 下面hi方法不是泛型方法
//2. 是hi方法使用了类声明的 泛型
public void hi(T t) {
}
//泛型方法,可以使用类声明的泛型,也可以使用自己声明泛型
public<K> void hello(R r, K k) {
System.out.println(r.getClass());//ArrayList
System.out.println(k.getClass());//Float
}
}
1.泛型不具备继承性
List
2.>
:支持任意泛型类型
3. extends A>
:支持A类以及A类的子类,规定了泛型的上限
4. super A>
:支持A类以及A类的父类,不限直接父类,规定了泛型的下限
>
支持任意通配符 extends A>
支持A类以及A类的子类,规定是上限 super A>
支持A类的父类,规定了下限
package collection_.collectionP.generic_.generic_01;
import java.util.ArrayList;
import java.util.List;
/**
* @author: 海康
* @version: 1.0
*/
public class GenericExtends {
public static void main(String[] args) {
ArrayList<Object> list1 = new ArrayList<>();
ArrayList<String> list2 = new ArrayList<>();
ArrayList<AA> list3 = new ArrayList<>();
ArrayList<BB> list4 = new ArrayList<>();
ArrayList<CC> list5 = new ArrayList<>();
// ? 表示了可以接受任意类型 是一个通配符
printCollection01(list1);
printCollection01(list2);
printCollection01(list3);
printCollection01(list4);
printCollection01(list5);
// ? extends AA 表示上限,可以接受 AA 或 AA子类
// printCollection02(list1); 错误
// printCollection02(list2); 错误
printCollection02(list3);
printCollection02(list4);
printCollection02(list5);
// ? super AA 表示下限,可以接受 AA 的父类,不限于直接父类,规定下限
printCollection03(list1);
// printCollection03(list2); 错误
printCollection03(list3);
// printCollection03(list4); 错误
// printCollection03(list5); 错误
}
// 通配符,取出时,就是Object
public static void printCollection01(List<?> list) { }
// ? extends AA 表示上限,可以接受 AA 或 AA子类
public static void printCollection02(List<? extends AA> list){ }
// ? super AA 表示下限,可以接受 AA 的父类,不限于直接父类,规定下限
public static void printCollection03(List<? super AA> list){ }
}
class AA {}
class BB extends AA{}
class CC extends BB{}
Collection
接口 特点 方法
Collection
接口的子接口:List
实现类:
ArrayList
LinkedList
Vector
Collection
接口的子接口:Set
实现类 :HashSet
TressSet
LinkedHashSet
注意是:Collection
接口的父接口是Iterable
接口
Map
接口 特点 方法 遍历方式
Map
接口的实现类 :HashMap
Hashtable
等
Collections
工具类的使用
特点:
Collection
实现子类可以存放多个元素,每个元素可以是Object
Collection
的实现类,可以存放重复的元素,有些不可以Collection
的实现类,有些是有序的【List
】,有些是无序的【Set
】Collection
接口没有直接的实现子类,是通过它的子接口Set
和 List
来实现的Collection
接口常用方法1. add:添加单个元素
2. remove:删除指定元素
3. contaions:查找元素是否为空
4. size:获取元素个数
5. isEmppty:判断是否为空
6. clear:清空
7. addAll:添加多个元素
8. contaionsAll:查找多个元素是否都存在
9. removeAll:删除多个元素
10. toArray:返回Object[]
使用Collections
工具类提供的synchronizedxxx()
方法,将这些集合类包装成线程安全的集合类
List
接口和常见方法List
接口基本介绍:
List
集合类中元素有序【即添加顺序和取出顺序一致】并且可以重复List
集合中的每个元素都有其对应的顺序索引,即支持索引List
容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素List
接口可以存放null
List
接口常用方法List
集合里添加了一些根据索引来操作集合元素的方法
插入
1. boolean add(Object ele):在集合中尾部插入ele元素
1. void add(int index,Object ele):在index位置插入ele元素
2. boolean addAll(int index,Collection eles):在index位置开始将eles中的所有元素添加进来
查询
3. Object get(int index):获取指定index位置的元素
4. int indexOf(Object obj):返回obj在集合中首次出现的位置
5. int lastIndexOf(Object obj):返回obj在当前集合中未次出现位置
移除
6. Object remove(int index):移除指定index位置的元素,并返回此元素
替换
7. Object set(int index,Object ele):设置指定index位置的元素为ele,相当于是替换
截取
8. List subList(int fromIndex,int toIndex):rfromIndex到toIndex位置的子集合【相当于截取成子集合】
List
的三种遍历方式方式一:使用iterator
【迭代器方式】
方式二:使用增强for
循环
方式三:使用普通for
循环
【使用普通 for
循环需要使用两个方法:一个获取多少个元素【size()方法
】,一个是获取元素方法【get(int index)
】】
ArrayList
底层 ArrayList<String> integers = new ArrayList<>();
for (int i = 0; i < 3; i++) {
integers.add("A_"+i);
}
源码分析:
1.先创建一个 ArrayList 对象 并完成初始化大小为0,当第一次添加元素时,扩容为10大小,以后扩容时,按照1.5倍扩容
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; 创建一个空数组
transient Object[] elementData; 为null 说明ArrayList底层是维护了一个 elementData 数组
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;将空数组赋给 elementData
所以使用是无参构造创建 ArrayList 对象时 开始大小是 0
}
2.添加元素
private int size; // 当集合中没有元素时 size 为 0
public boolean add(E e) {
在添加前先确定当前容量是否能够添加一个元素
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e; 将当前的元素添加到elementData数组指定索引的位置
return true;
}
3.确保当前的容量大小
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static final int DEFAULT_CAPACITY = 10;
private static int calculateCapacity(Object[] elementData, int minCapacity) {
如果是第一次添加时,先确定elementData是否是一个空数组,则 if 语句成立,将返回一个 DEFAULT_CAPACITY 的值就是 10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
确保是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++; 记录当前ArrayList集合被修改的次数
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
扩容代码如下
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length; 第一次添加时 elementData是0
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);
}
所以结论是:当使用无参构造器创建对象时,第一次添加时,扩容的大小为10,当再次扩容时,按照以前的1.5倍
总结:
第一步:先创建一个ArrayList
对象,并且ArrayList
底层维护是一个Object[]
数组,初始化大小为0,
第二步:当我们添加一个元素时,就是调用add
方法时,在add
方法中先调用ensureCapacityInternal(size + 1);
方法判断当前容量是否能够添加一个元素
第三步:由于是第一次添加元素时,Object
数组为空,所以if
成立,返回一个默认大小为10
private static final int DEFAULT_CAPACITY = 10;
private static int calculateCapacity(Object[] elementData, int minCapacity) {
如果是第一次添加时,先确定elementData是否是一个空数组,则 if 语句成立,将返回一个 DEFAULT_CAPACITY 的值就是 10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
第四步:
确保是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
modCount++; 记录当前ArrayList集合被修改的次数
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
第五步:
扩容代码如下
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length; 第一次添加时 elementData是0
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);
}
如果使用的是指定大小的构造器,则初始 elementData 容量为指定大小,如果需要扩容,则直接扩容 elementData 为 1.5 倍
ArrayList
的底层操作机制源码分析【必须掌握】【背下】结论:
1.ArrayList
中维护了一个Object
类型的数组elementData
。transient Object[] elementData;
transient
表示瞬间,短暂的,表示该属性不会被序列号
2.当创建ArrayList
对象时,如果使用的是无参构造,则被始elementData
容量为0,第1次添加,则扩容elementData
为10,如需要再次扩容,则扩容elementData
为1.5倍
3.如果使用的是指定大小的构造器,则初始elementData
容器为指定大小,如果需要扩容,则直接扩容elementData
为1.5倍
面试回答:
ArrayList
底层是用数组实现的存储的,并且在底层维护着一个Object[]
数组,ArrayList
特点是查询效率高,查询效率高的因为是提交get()
方法根据索引查询,增删效率低原因是要移到数组元素的位置,并且ArrayList
是线程不安全的,在一般情况下,如果大多数业务都是查询,则可以使用ArrayList
因为效率高,【如果涉及到增删比较频繁则就可以选择LinkedList
,如果需要线程安全则可以使用Vector
】
例如:添加元素
第一步:ArrayList
底层是维护着一个Object[]
数组 ,初始化大小为0
第二步:当我们添加一个元素时,就是调用add
方法时,在add
方法中先调用ensureCapacityInternal(size + 1);
【英说可怕S体英讨了:】方法判断当前容量是否能够添加一个元素
第三步:在ensureCapacityInternal
方法中,会先调用calculateCapacity
【客Q里可怕S体】方法,比如是第一次添加,返回是默认大小10
,如果不是第一次添加,则返回是当前容量大小
第四步:调用确定是否扩容方法ensureExplicitCapacity
【英说 姨:S婆喜:讨 可怕S体】方法传入当前calculateCapacity
返回的大小,在该方法调用grow
【哥老】方法确定是否要扩容
Vector
底层
步骤1:
使用是无参构造创建 Vector 对象 默认 在底层创建一个 大小为10 protected Object[] elementData;数组
public Vector() {
this(10);
}
步骤2:
public synchronized boolean add(E e) {
modCount++;// 对象 Vector修改的次数
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
步骤3: 确定是否需要扩容
private void ensureCapacityHelper(int minCapacity) {
// 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 + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);//将原来的数组拷贝到新的数组中
}
面试回答:
Vector
底层在底层维护是一个Object[]
数组
Vector
是线程同步的,即线程安全的,Vector
类中操作方法都带有synchronized
关键字
如果在开发中,需要线程同步安全时,就可以考虑使用Vector
Vector
集合可以添加null
值
例如:添加元素
第一步:使用是无参构造创建 Vector 对象 默认 在底层创建一个 大小为10 protected Object[] elementData;数组,在构造器使用
this(10)
第二步:调用add
方法,并且在add
方法中有一个属性modCount
记录对象Vector
修改的次数
第三步:确定是否需要扩容 ,如果当前容器大小再加1,大于当前容器大小时,就进行扩容 ,扩容调用grow
方法
第四步:扩容算法,大小不够添加元素时,是按照两位扩容 的
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
其实在ArrayList
和Vector
底层扩容方法要都到Arrays
工具类中的cpoy
方法,在cpoy
方法中其实使用到了System.arraycopy
方法
LinkedList
底层LinkedList
的全面说明:
LinkedList
底层实现了双向链表和双端队列特点null
LinkedList
的底层操作机制
LinkedList
底层维护了一个双向链表LinkedList
中维护了两个属性first
【宝s】和last
【辣s】分别指向 首节点 和 尾节点Node
对象),里面又维护了prev
【婆s】【指向上一个节点】 next
【奶s】【指向下一个节点】 item
【爱等】三个属性,其中通过prev
指向前一个,通过next
指向后一个节点,最终实现双向链表LinkedList
元素添加和删除,不是通过数组完成的,相对来说效率较高。 步骤1:使用无参构造器创建对象
public LinkedList() {
}
会对 transient int size = 0; 属性进行初始化,表示当前大小为0
步骤2:添加方法
public boolean add(E e) {
linkLast(e);
return true;
}
步骤3:添加方法
transient Node<E> last;
void linkLast(E e) {
final Node<E> l = last;链表最后一个元素赋给 l
final Node<E> newNode = new Node<>(l, e, null);
last = newNode; 将last指向新添加元素
if (l == null) 当前集合为空,就使first指向第一个元素
first = newNode;
else 否则 l 指向下一个元素
l.next = newNode;
size++; 表示当前集合的大小
modCount++; 表示当前集合被修改的次数
}
调用内容类,完成相关属性初始化
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
Set
集合Set
接口基本介绍无序【添加和取出的顺序不一致】
不允许重复元素,所以最多包含一个null
【就是可以存放一个null
】
注意:取出的顺序虽然不是添加的顺序,但是它取出顺序是固定的
List
集合和Set
集合区别List
代表是有序的,元素可以重复
Set
代表是无序的,元素不可以重复
HashSet
集合分析HashSet
底层是HashMap
,HashMap
底层是【数组+链表+红黑树】
下面的结论非常重要必须记住【背下】
HashSet
底层是HasMap
,因为在HashSet
的构造器创建了HashMap
对象,在HashhMap
中维护了一个Node
节点的table数组,并且默认初始化大小为16,负载因子大小为0.75【决定了链表达到多少时进行扩容】hashCode
方法】equals
判断,如果相等,则不再添加。如果不相等,则以链表的方式添加java8
中,如果一条链表的元素个数到达了TREEIFY_THRESHOLO
【默认是8】,并且table
的大小>=MIN_TREEIFY_CAPACITY
【默认64】,就会进行树化【红黑树】HashSet
底层就是HashMap
,第一次添加时,table
数组扩容到16,临界值threshold
是16*加载因子(loadFactor)是 0.75=12
table
数组使用到了临界值12,就会扩容到16*2=32
,新的临界值就是32*0.75=24
,依次类推java8
中,如果一条链表的元素个数到达了TREEIFY_THRESHOLD(默认是8)
,并且是table
的大小>=MIN_TREEIFY_CAPACITY(默认是64)
,就会进行树化【红黑树】,否则仍然采用数组扩容机制HastSet
添加元素第一步:当我们调用add
方法时,在add
方法中却调用了HashMap
中的put
方法,并且key
为我们传入元素,而value
值是一个占位符private static final Object PRESENT = new Object();
第二步:在put
方法中会先调用hash
方法获取元素哈尔值
第三步:调用putVal
方法将获取元素哈尔值作为参数传入,在putVal
方法先判断当前表是否为空和表长度是否为0,如果是调用resize
方法进行扩容,就是创建一个数组大小为16
第四步:根据元素的哈尔值与当前数组进行与
运算得到在table
表中的索引位置,如果该位置没有元素则直接添加,如果该元素已经有元素继续调用equals
方法,如果equals
值不相等时,则以链表的形式添加,如果equals
相等时,以不添加,并且将原来的value
值替换掉
步骤1:使用无参构造器:HashSet集合底层维护下面两个属性
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();// 这个属性在添加时当占位符使用
下面是使用无参构造创建一个HashSet对象,其实底层在HashSet构造器中创建一个 HashMap集合赋给 map
public HashSet() {
map = new HashMap<>();
}
步骤2:添加方法
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
调用 HashMap 中 put 方法 并且 PRESENT 是一个占位符,是直接 new Object()对象赋给 PRESENT
步骤3:调用 HashMap 中 put 方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
调用 hash 方法 获取一个 哈希值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
步骤4:真正添加方法
在 HashMap 类中 维护一个transient Node<K,V>[] table;属性第一添加时为null
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)判断当前表是否为空和表长度是否为0
如果是就调用 resize()方法进行扩容,就是创建一个数组
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)如果当前位置没有元素直接添加
tab[i] = newNode(hash, key, value, null);
else {如果有元素进行如下判断
Node<K,V> e; K k;
如果 hash 值和key指定向相同位置或equals值相同时 则不添加
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) {如果当前下一个位置为null时直接添加
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st、
判断一条链是否达到了8个,调用 treeifBin 方法,进行树化,注意同时还要table达到64
treeifyBin(tab, hash);
break;
}
如果两者相同则不添加
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
如果 key 相同时就替换值
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;
}
扩容算法
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
newCap = DEFAULT_INITIAL_CAPACITY;
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;
}
LinkedHashSet
的全面说明LinkedHashSet
是HashSet
的子类
LinkedHashSet
底层是一个LinkedHashMap
,在LinkedHashSet
使用super(16,0.75,true)
调用父类HashSet
,在HashSet
的构造中创建LinkedHashMap
对象,底层维护了一个数组+双向链表
LinkedHashSet
根据元素的hashCOde
值来决定元素的存储位置,同时使用链表维护元素的次序,这使元素看起来以插入顺序保存的
LinkedHashSet
不允许添加重复元素
在LinkedHashSet
中维护一个hash
表和双向链表【LinkedHashSet
有head
和tail
】
每一个节点有before
和after
属性,这样可以形成双向链表
在添加一个元素时,先求hash
值,在求索引,确定该元素在hashtable
的位置,然后将添加的元素加入到双向链表【如果已经存在,则不添加【原则和HashSet
一样】】
tail.next = new Element;//简单指定
newElement.prev = tail;
tail = newElement;
这样的话,我们遍历LinkedHashSet
也能确保插入顺序和遍历顺序一致
注意是由于底层维护着是一个双向链表和数组,所以插入和取出的顺序是一致的,原因是用了链表维护元素添加的顺序
添加元素:
第一步:添加add
方法时在底层是调用了 LinkedHashMap
的父类HashMap
中的put
方法
第二步:在put
方法中会先调用hash
方法获取元素哈尔值
第三步:调用putVal
方法将获取元素哈尔值作为参数传入,在putVal
方法先判断当前表是否为空和表长度是否为0,如果是调用resize
方法进行扩容,就是创建一个数组大小为16
第四步:根据元素的哈尔值与当前数组进行与
运算得到在table
表中的索引位置,如果该位置没有元素,则创建newNode
节点并且动态绑定到LinkedHashMap
的newNode
方法完成双向链表之间的连接,如果该元素已经有元素继续调用equals
方法,如果equals
值不相等时,则以链表的形式添加,如果equals
相等时,以不添加,并且将原来的value
值替换掉
Map
集合Map
集合是一个双列集合,一个元素包含两个值就是key-value
Map
集合中的元素,key
和value
可以为任何引用类型数据map
集合中key
不允许重复,而value
值可以重复,key
值可以为null
但是只有有一个,而value
值可以为任意值key
和value
之间存在单向一对一关系,即通过指定的key
总能找到对应的value
HashMap
集合扩容机制【和HashSet
相同】到达临界值都是以2倍的方式进行扩容
HashMap
底层维护了Node
类型的数组table
,默认为null
loadfactor
初始化为0.75
key-value
时,通过key
的哈希值得到在table
的索引位置,然后判断该索引处是否有元素,如果没有元素直接添加。如果该索引处有元素,继续判断该元素的key
和准备加入的key
是否相等,如果相等,则直接替换掉value
值,如果不相等需要判断,是树结构还是链表结构,做出相应处理。如果添加时发现容量不够,则需要扩容table
容量为16,临界值threshole为12(16*0.75)
table
容量为原来的2倍(32)
,临界值为原来的2倍,即24,依次类推java8
中如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认是8)
,并且table
的大小>=MIN_TREEIFY_CAPACITY(默认64)
,就会进行树化【红黑树】Hashtable
集合K-V
,底层维护是Entry
【n吹】数组Hashtable
的键和值都不能存放null
,否则会报空指针异常Hashtable
使用方法基本上和HashMap
一样Hashtable
是线程安全的,HashMap
是线程不安全的int newCapacity = (oldCapacity << 1) + 1;// 左移再加1
添加元素:
第一步:在创建Hashtable
对象时,使用无参构造器创建对象,会在底层创建一个大小为11
的table=new Entry,?>[initialCapacity];
的数组
第二步:调用put
方法,在put
方法中称判断value
值是否为null
,如果为空则会抛出NullPointerException()
异常
第三步:获取key
的哈尔值,并且计算在table
数组中的位置(hash & 0x7FFFFFFF) % tab.length;计算出在数组中索引位置
第四步:获取当前索引位置的元素是,是否为null,如果不为null,则进入for进行相关的判断
第五步:如果要添加的元素和原来的元素 哈希值和equals值,相同时,则将原来的value替换掉,并返回
Properties
集合Properties
类继承自Hashtable
类并且实现了Map
接口,也是使用一种键值对的形式来保存数据Hashtable
类似Properties
是Hashtable
的子类所以键值都不可以存放null
值,否则会报空指针异常Properties
可以有相同的key
,则原先的value
值也会被替换掉Properties
还可以用于从xxx.properties
文件中,加载数据到Properties
类对象,并进行读取和修改xxx.properties
文件通常作为配置文件,这个知识占在IO
流举例TreeSet
集合基本介绍:
当我们使用无参构造器,创建TreeSet
时,仍然是无序的
当我们需要按照某种方式进行排序时,需要使用TreeSet
提供的一个构造器,可以传入一个比较器【匿名内部类】并指定排序规则
添加元素:
第一步:TreeSet
底层是TreeMap
,在TreeSet
的构造中会创建TreeMap
对象,并且对比较器初始化private final Comparator super K> comparator;
第二步:执行add
方法,在add
方法中调用了TreeMap
中的put
方法,传入元素和PRESENT[占位符]
//PRESENT其实是 :private static final Object PRESENT = new Object();
第三步:判断当前集合是否为空时,如果为空将key
传入compare(key,key)
方法中,比较,将元素添加到集合中,记录集合大小为1,modCount
加1
第四步:判断比较器的值是否为空,如果不为null
,则使用比较器的规则进行比较,比较值返回小于
0时,则放在左边,大于0放在右边,相等时,则将原来的替换掉,并且返回原来的值
第五步:判断比较器的值为空时,则使用默认的比较规则
注意:当使用比较器时,比较器比较后返回的值相等,则将原来的value值替换掉,并返回原来的value值
源码剖析:
第一步:
TreeSet底层是TreeMap,所以在TreeMap中完成:对比较器的初始化:private final Comparator<? super K> comparator;
在追TreeSet的构造顺底层其实TreeMap
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
第二步:执行add方法
public boolean add(E e) {
return m.put(e, PRESENT)==null;
//PRESENT其实是 :private static final Object PRESENT = new Object();
}
往put方法中继续追
public V put(K key, V value) {//传入key 和 PRESENT [PRESENT其实是占位符]
Entry<K,V> t = root;// private transient Entry root; 当集合为空时,root必须为空
if (t == null) {//这里是当集合为空时,才会执行
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1; 表示集合大小为了1
modCount++; 表示修改了一次
return null; 返回null值表示修改成功
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator; // 将比较器引用赋给 cpr
if (cpr != null) { 如果比较器不为 null
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0) 当比较小于 0 则放在左边
t = t.left;
else if (cmp > 0) 当比较大于 0 则放在右边
t = t.right;
else //如果比较器返回的值相等,则将原来的替换掉,并返回原来的值
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;
}
public V setValue(V value) { //如果比较器返回的值相等,则将原来value的替换掉,并返回原来的值value
V oldValue = this.value;
this.value = value;
return oldValue;
}
}
}
TreeMap
集合基本介绍:
当我们使用无参构造器,创建TreeMap
时,仍然是无序的
当我们需要按照某种方式进行排序时,需要使用TreeMap
提供的一个构造器,可以传入一个比较器【匿名内部类】并指定排序规则
添加元素:
第一步:创建TressMap
时,并且对比较器初始化private final Comparator super K> comparator;
第二步:执行add
方法,在add
方法中调用了TreeMap
中的put
方法,传入key
和value
值
第三步:判断当前集合是否为空时,如果为空将key
传入compare(key,key)
方法中,比较,将元素添加到集合中,记录集合大小为1,modCount
加1
第四步:判断比较器的值是否为空,如果不为null
,则使用比较器的规则进行比较,比较值返回小于
0时,则放在左边,大于0放在右边,相等时,则将原来的替换掉,并且返回原来的值
第五步:判断比较器的值为空时,则使用默认的比较规则
注意:当使用比较器时,比较器比较后返回的值相等,则将原来的value值替换掉,并返回原来的value值
解读源码:
1. 构造器. 把传入的实现了 Comparator接口的匿名内部类(对象),传给给TreeMap的comparator
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
2. 调用put方法
2.1 第一次添加, 把k-v 封装到 Entry对象,放入root
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
2.2 以后添加
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do { //遍历所有的key , 给当前key找到适当位置
parent = t;
cmp = cpr.compare(key, t.key);//动态绑定到我们的匿名内部类的compare
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else //如果遍历过程中,发现准备添加Key 和当前已有的Key 相等,就不添加
return t.setValue(value);
} while (t != null);
}
Collections
工具类基本介绍:
Collections
是一个操作Set
List
和 Map
等集合的工具类Collections
中提供一系静态的方法对集合元素进行排序 , 查询和修改等操作排序操作
1. reverse(List):反转List中元素的顺序
2. shuffle(List):对List集合元素进行随机排序
3. sort(List):根据元素的自然顺序对指定List集合元素按升序排序
4. sort(List,Comparator):根据指定的Comparator产生的顺序对List集合元素进行排序
替换 和 查找操作
5. swap(List, int ,int ):将指定List集合中的 i 处元素 和 j 处元素进行交换
6. Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
7. Object max(Collection, Comparator):根据Comparator指定的顺序,返回给集合中的最大元素
8. Object min(Collection):根据元素的自然顺序,返回给定集合中的最小元素
9. Object min(Collection, Comparator):根据Comparator指定的顺序,返回给集合中的最大元素
10. int frequency(Collection,Object):返回指定集合中指定元素的出现次数
11. void copy(List dest,List src):将src中的内容复制到dest中
注意复制的目标集合的长度必须大于源集合,否则会抛出空指针异常
12. boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换List对象的所有旧值
13.将线程不安全的集合转成线程安全