文章目录
- 前言
- 封装
- 继承
- 多态
- 抽象方法
- 接口
- 内部类
- static
- 权限修饰符
- this super
- private关键字
- final关键字
- 就近原则
- 构造方法
- ==号
- StringBuilder
- StringJoiner
- 字符串原理总结:
- 1、字符串存储的内存原理
- 2、==号比较的是什么?
- 3、字符串拼接的底层原理
- 4、StringBuilder提高效率的原理
- StringBuilder源码分析
- Arrays
- Lambda表达式
- 集合
- 集合体系结构
- Collection【单列集合】
- Collection的遍历方式
- 增强for遍历
- Lambda表达式遍历
- List集合
- List的遍历方式
- List实现类
- ArrayList常用方法【有序可重复】
- ArrayList集合底层原理
- LinkedList常用方法【有序可重复】
- 泛型
- 红黑树
- Set集合
- HashSet集合
- LinkedHashSet集合
- TreeSet集合
- Map集合【双列集合】
- map集合的遍历方式
- HashMap集合
- LinkedHashMap集合
- TreeMap集合
前言
封装
封装:正确设计对象的属性和方法
原则:对象代表什么,就得封装对应的数据,并提供数据对应的行为
封装思想的好处:
- 让编程变得很简单,有什么事,找对象,调方法就行
- 降低学习成本,有需要时去找就行
继承
继承【自己设计+用别人的】:Java中提供一个关键字extends,用这个关键字,我们可以让一个类和另一个类建立起继承关系
优点:
- 可以把多个子类中重复的代码抽取到父类中,提高代码的复用性
- 子类可以在父类的基础上,增加其他的功能,使子类更强大
什么时候用继承? 当类和类之间,存在相同的内容,并满足子类是父类中的一种,就可以考虑使用继承,来优化代码
特点:
- Java只支持单继承:一个子类只能有一个父类
- Java不支持多继承,但支持多层继承【子类A继承父类B,父类B可以继承父类C,C是A的间接父类 -->祖孙三代】
- Java中每一个类都直接或者间接的继承于Object类
- 子类只能访问父类中非私有的成员
子类能继承父类中的哪些内容:
父类的构造方法不能被继承
父类中的成员变量可以被继承【非私有 private都可以 只是private修饰的成员变量不能用而已】
只有父类中的虚方法才能被子类继承
【虚方法:非private 非static 非final】
继承中成员变量的访问特点:
就近原则。先在局部位置找,本类成员位置找,父类成员位置找,逐级往上
继承中成员方法的访问特点:
同成员变量,就近原则
继承中构造方法的访问特点:
父类中的构造方法不会被子类继承,但是可以通过super调用
子类中所有的构造方法默认先访问父类中的无参构造,再执行自己
为什么?
子类在初始化之前,有可能会使用到父类中的数据,如果父类没有完成初始化,子类将无法使用父类的数据
子类初始化之前,一定要调用父类构造方法先完成父类数据空间初始化
怎么调用父类构造方法?
子类构造方法的第一行默认语句都是:super()
。不写也存在。且必须在第一行
方法重写
当父类的方法不能满足子类现在的需求时,需要进行方法重写
方法重写注意事项和要求
1、重写方法的名称、形参列表必须与父类中保持一致
2、子类重写父类方法时,访问权限子类必须大于等于父类(缺省<protected<public)
3、子类重写父类方法时,返回值类型必须小于等于父类
4、只有被添加到虚方法表中的方法才能被重写
多态
多态:同类型的对象,表现出的不同形态
多态的前提:
- 有继承/实现关系
- 父类型引用指向子类型对象
- 有方法重写
多态的好处:
使用父类型作为参数,可以接收所有子类对象,体现多态的扩展性与便利
调用成员变量:编译看左边,运行也看左边
调用成员方法:编译看左边,运行看右边
多态的优势:
- 在多态形势下,右边对象可以实现解耦合,便于扩展和维护
- 定义方法的时候,使用父类型作为参数,可以接受所有子类对象,体现多态的扩展性与遍历
多态的弊端:
不能调用子类的特有功能
原因?
当调用成员方法的时候,编译看左边,运行看右边,在编译的时候会先检查左边类中的有没有这个方法,如果没有直接报错
解决方案:
变回子类型即可
有可能会发生转换类型与真实对象类型不一致的问题,因此在转换的时候可以使用关键字instanceof关键字判断
抽象方法
抽象方法:将共性的行为(方法)抽取到父类之后,由于每一个子类执行的内容是不一样的,所以,在父类中不能确定具体的方法体,该方法就可以定义为抽象方法
抽象类:如果一个类中存在抽象方法,那么该类就必须声明为抽象类
抽象方法和抽象类的注意事项
- 抽象类不能实例化
- 抽象类中不一定有抽象方法,由抽象方法的类一定是抽象类
- 可以有构造方法
不能创建对象有什么用呢?当创建子类对象时,给属性进行赋值
- 抽象类的子类要么重写抽象类中的所有抽象方法,要么是抽象类
抽象方法和抽象类的意义
强制子类必须按照这种格式进行重写
接口
接口:就是一种规则【侧重于行为】,是对行为的抽象
接口类的注意事项
- 接口和类的实现关系,可以单实现,也可以多实现
- 实现类还可以在继承一个类的同时实现多个接口
接口中成员的特点
成员变量:只能是常量 默认修饰符public static final
构造方法:没有
成员方法:在JDK7之前 接口中只能定义抽象方法默认修饰符public abstract
【只要接口中发生变化,所有的实现类都要发生变化】
【又想加新的方法,又不想报错】
JDK8以后接口中新增的方法:
允许在接口中定义默认方法,需要使用关键字
default
修饰
允许在接口中定义静态方法,需要使用关键字static
修饰
作用:解决接口升级问题
接口中默认方法的注意事项:
1、默认方法不是抽象方法,所以不强制被重写。但是如果被重写,重写的时候去掉default
关键字
2、public
可以省略,default
不能省略
3、如果实现了多个接口,多个接口中存在相同名字的默认方法,子类就必须对该方法进行重写
接口中静态方法的注意事项:
静态方法不需要重写
JDK9以后接口中新增的方法:
接口中可以定义私有方法【普通的私有方法、静态的私有方法】,为了解决JDK8中允许定义的默认和静态方法有一些重复的代码需要提取出来,而提取出来的代码又不想被外界直接调用,定义私有方法
接口和类之间的关系
类和类的关系
继承关系,只能单继承,不能多继承,但是可以多层继承
类和接口的关系
实现关系,可以单实现,也可以多实现,还可以在继承一个类的同时实现多个接口
接口和接口的关系
继承关系,可以单继承,也可以多继承
内部类
内部类:下载一个类里面的类就叫做内部类
什么时候用到内部类?B类表示的事物是A类的一部分,且B单独存在没有意义,汽车的发动机,ArrayList的迭代器
分类:成员内部类、静态内部类、局部内部类、匿名内部类
成员内部类
静态内部类
局部内部类
匿名内部类
匿名内部类:隐藏了名字的内部类,可以写在成员位置,也可以写在局部位置
格式的细节:包含了继承或实现,方法重写,创建对象
整体就是一个类的子类对象或者接口的实现类对象
使用场景:当方法的参数是接口或者类时,以接口为例,可以传递这个接口的是西安类对象,如果实现类只要使用一次,就可以用匿名内部类简化代码
在成员内部类中,JDK16之前不能定义静态变量,JDK16之后才可以定义静态变量
static
静态变量
(在堆内存当中会开辟一个静态存储位置【静态区】 对象共享的,在内存当中只有一份)
static表示静态,是Java中的一个修饰符,可以修饰成员方法,成员变量
特点: 被该类所有对象共享;不属于对象属于类;静态变量是随着类的加载而加载,优先于对象出现的
调用方式:类名调用;对象名调用
静态方法
特点:多用在测试类和工具类中;JavaBean类中很少会用
调用方式:类名调用;对象名调用
static注意事项
静态方法只能访问静态变量和静态方法
非静态方法可以访问所有
静态方法中没有this关键字
权限修饰符
权限修饰符:是用来控制一个成员能都被访问的范围的
this super
this:理解为一个变量,表示当前方法调用者的地址值
super:代表父类存储空间
private关键字
1、权限修饰符
2、可以修饰成员(成员变量或者方法)
3、被private修饰的成员只能在本类中才能访问
4、针对private修饰的成员变量,如果需要被其他类使用,提供相应的操作
5、提供setXxx(参数)
方法,用于给成员变量赋值,方法用public修饰
6、提供getXxx(参数)
方法,用于获取成员变量的值,方法用public修饰
final关键字
final修饰方法:表明该方法是最终方法,不能被重写
final修饰类:表示该类是最终类,不能被继承
final修饰变量:叫做常量,不能被修改
细节:
- final修饰的变量是基本类型:那么变量存储的数据值不能发生改变。
- final修饰的变量是引用类型:那么变量存储的地址值不能发生改变,对象内部可以改变
就近原则
就近原则:谁离我更近,我就用谁
第一个age会先在局部位置中去找,如果能找到,那么它使用的就是局部位置的age
第二个age直接使用成员遍历位置的age
this的作用:区分成员变量和局部变量
this的本质:代表方法调用者的地址值
构造方法
构造方法也叫做构造器、构造函数
作用:在创建对象时,由虚拟机自动调用,给成员变量进行初始化
特点:
1、方法名要和类名完全一致
2、没有返回值类型,void也没有
3、没有具体的返回值(不能由return带回结果数据)
构造方法的定义:
1、如果没有写任何的构造方法,那么虚拟机会给我们加一个无参构造方法
2、如果定义了构造方法,系统将不再提供默认的构造方法
==号
==号
比较的到底是什么?
1、基本数据类型:比较的是具体的数据值
2、引用数据类型:比较的是地址值
那么字符串比较应该用什么呢?
boolean equals
完全一样结果才是true,否则为false;
boolean equalsIgnoreCase
忽略大小写比较【验证码输入】
键盘输入的字符串实际上是new出来的
StringBuilder
StringBuilder
可以看成是一个容器,创建之后里面的内容是可变的
StringJoiner
StringJoiner
跟StringBuilder
一样,也可以看成是一个容器,创建之后里面的内容是可变的。
作用:提高字符串的操作效率,而且代码编写特别简洁,但目前市场上很少有人使用
JDK8之后才出现
字符串原理总结:
1、字符串存储的内存原理
直接复制会复用字符串常量池中的
new出来不会复用,而是开辟一个新的空间
2、==号比较的是什么?
基本数据类型比较数据值
引用数据类型比较地址值
3、字符串拼接的底层原理
等号右边没有变量
String s1 = "abc"
拼接的时候没有变量,都是字符串。触发字符串的优化机制,在编译的时候就已经是最终的结果了,会复用串池中的字符串
等号右边有变量
String s1 = "abc"
;
String s2 = "d"
;
String s3 = "ab"+s2
;
JDK8以前:系统底层会自动创建一个StringBuilder对象,然后再调用其append方法完成拼接。拼接后,再调用其toString方法转换为String类型,而toString方法的底层是直接new了一个字符串对象。
一个加号,堆内存中俩对象【StringBuilder String
】,浪费时间,浪费性能
JDK8以后:系统会预估字符串拼接之后的总大小,把要拼接的内容都放在数组中,此时也是产生一个新的字符串。
4、StringBuilder提高效率的原理
把所有的东西都往StringBuilder
这个容器中去放,不会创建很多无用的空间,节约内存
疑问?会撑爆吗?
StringBuilder默认容量是16
默认创建一个长度为16的字节数组
添加的内容长度小于16,直接存
添加的内容大于16会扩容:2*老容量+2 = 34
如果超出扩容的容量,则以实际长度为准
StringBuilder源码分析
默认容量
append方法
进入这个方法 :先会进行一次非空判断,如果不是null
1、先获取到现在添加的字符的长度
2、去判断是否需要扩容
进入这个方法 :
拿着3去减老容量16,小于0不需要扩容
假设需要扩容 str = “a-z” 26个英文字母
此时26-10>0,需要扩容
进入这个方法 :
进入这个方法 :
16+18 也就相当于 2*老容量+2 = 34 完成扩容
Arrays
操作数组的工具类
binarySearch
:二分法查找元素
细节一:二分法查找的前提:数组中的元素必须是有序的,数组中的元素必须是升序的
细节二:如果要查找的元素是存在的,那么返回的是真实的索引,否则返回的是-(插入点-1)
copyOf
:拷贝数组
细节:如果新数组的长度是小于老数组的长度,会部分拷贝
细节:如果新数组的长度是大于老数组的长度,会补上默认初始值
copyOfRange
:拷贝数组(指定范围)
细节:包头不包尾,包左不包右
sort
:按照指定的规则排序
第二个参数是一个接口,所以我们在调用方法的时候,需要传递这个接口的实现类对象,作为排序规则,采用匿名内部类的方式
底层原理:利用插入排序+二分查找的方式进行排序的。默认把0索引的数据当作是有序的序列,1索引到最后认为是无序的序列。遍历无须的序列里得到的每一个元素,假设当前遍历得到的元素是A元素,把A往有序序列中插入,在插入的时候,是利用二分查找确定A元素的插入点。
拿着A元素和插入点的元素进行比较,比较的规则就是compare方法的方法体,如果方法的返回值是负数,拿着A继续跟前面的数据进行比较;如果方法的返回值是正数,拿着A继续和后面的元素比较;如果方法的返回值是0,也拿着A元素和后面的元素进行比较,直到能确定A的最终位置为止
compare方法的形式参数:
参数一 o1:表示在无序序列中,遍历得到的每一个元素
参数二 o2:表示有序序列中的元素
返回值:
负数:表示当前要插入的元素是小的,放在前面
正数:表示当前要插入的元素是大的,放在后面
0:表示当前要插入的元素跟现在的元素比是一样的,也会放在后面
结论:
o2- o1:降序排列
o1- o2:升序排列
Lambda表达式
函数式编程是一种思想特点
函数式编程思想,忽略面向对象的复杂语法,强调做什么,而不是谁去做
作用:
简化函数式接口的匿名内部类的写法
注意点:
Lambda表达式可以用来简化匿名内部类的书写
Lambda表达式只能简化函数式接口的匿名内部类写法
函数式接口:有且仅有一个抽象方法的接口叫做函数式接口,接口上方可以加@FunctionalInterface
注解
省略规则:
1、参数类型可以省略不写
2、如果只有一个参数,参数类型可以省略 同时()也可以省略
3、如果Lambda表达式的方法体只有一行,大括号,分号,return可以省略不写,需要同时省略
public static void main(String[] args) throws ParseException {
Integer[] arr = {2,1,4,5,3,7,8,9};
//匿名内部类的完整格式
Arrays.sort(arr,new Comparator<Integer>(){
@Override
public int compare(Integer o1, Integer o2) {
return o1-o2;
}
});
//lambda表达式的完整格式
Arrays.sort(arr,(Integer o1, Integer o2) ->{
return o1-o2;
}
);
//lambda表达式的省略写法
Arrays.sort(arr,(o1, o2) ->o2-o1);
System.out.println(Arrays.toString(arr));
}
例题:
定义数组并存储一些字符串,利用Arrays中的sort方法进行排序
要求:按照字符串的长度进行排序,短的在前面,长的在后面
public static void main(String[] args) throws ParseException {
String[] arr2 = {"a","aaaa","aaa","aa"};
//匿名内部类的完整格式
Arrays.sort(arr2, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.length()-o2.length();
}
});
//lambda表达式的完整格式
Arrays.sort(arr2, (String o1, String o2)-> {
return o1.length()-o2.length();
}
);
//lambda表达式的省略写法
Arrays.sort(arr2, (o1, o2)-> o1.length()-o2.length());
System.out.println(Arrays.toString(arr2));
}
集合
集合和数组的对比:
1、长度:数组长度固定;集合长度可变
2、存储类型:数组可以存基本数据类型,也可以存引用数据类型;集合可以存引用数据类型【基本数据类型需要转换为包装类】
集合体系结构
- List系列集合:有序可重复,有索引 (存和取的顺序是一样的)
- Set系列集合:无序不可重复,无索引 (存和取的顺序是不一样的)【可以用来进行数据去重】
Collection【单列集合】
Collection是单列集合的祖宗接口,它的功能是全部单列集合都可以继承使用的
Collection的遍历方式
- 迭代器遍历
迭代器在Java中的类是Iterator,是集合专用的遍历方式
public static void main(String[] args) throws ParseException {
Collection<String> coll = new ArrayList<>();
coll.add("aaa");
coll.add("bbb");
coll.add("ccc");
coll.add("ddd");
//获取迭代器对象
Iterator it = coll.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
}
细节:
- 迭代器遍历完毕指针不会复位。如果要继续第二次遍历集合,只能再次获取一个新的迭代器对象
- 循环中只能用一次next方法 【next方法获取元素并且移动指针】
- 迭代器遍历时,不能用集合的方法进行删除或增加。【会报
ConcurrentModificationException
错误
并发修改异常】,如果非要删除 可以使用迭代器提供的remove方式去删除,如果要添加,暂时没有办法 - 迭代器的指针已经指向了最后没有元素的位置时会报
NoSuchElementException
,为什么不是索引越界,因为迭代器在遍历集合的时候是不依赖于索引的
增强for遍历
增强for的底层就是迭代器,是在JDK5之后出现的,所有的单列集合和数组才能用增强for进行遍历
public static void main(String[] args) throws ParseException {
Collection<String> coll = new ArrayList<>();
coll.add("aaa");
coll.add("bbb");
coll.add("ccc");
coll.add("ddd");
for (String c:coll){
System.out.println(c);
}
}
细节:
- 修改增强for中的变量,不会改变集合中原本的数据
Lambda表达式遍历
JDK8之后
先采用匿名内部类的方式
public static void main(String[] args) throws ParseException {
Collection<String> coll = new ArrayList<>();
coll.add("aaa");
coll.add("bbb");
coll.add("ccc");
coll.add("ddd");
//2、利用匿名内部类的方式实现
//底层原理
coll.forEach(new Consumer<String>() {
@Override
//s依次表示集合中的每一个数据
public void accept(String s) {
System.out.println(s);
}
});
}
foreach底层原理
自己也会遍历集合,依次得到每一个元素,把得到的每一个元素,传递给下面的accept方法
采用Lambda方式
public static void main(String[] args) throws ParseException {
Collection<String> coll = new ArrayList<>();
coll.add("aaa");
coll.add("bbb");
coll.add("ccc");
coll.add("ddd");
//3、利用lambda方式实现
coll.forEach( s-> System.out.println(s));
}
List集合
Collection的方法List都继承了,List因为有所有,所以多了很多索引操作的方法
List集合中两个删除的方法:
1、直接删除元素
2、通过索引进行删除
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
//此时删除的是索引下标为1的元素 而不是数据为1的元素
list.remove(1);
因为在调用方法的时候,如果出现了方法重载,会优先调用实参和形参类型一致的方法
如果想删除数据元素1
list.remove(0);
//或者
//需要进行手动装箱
Integer i = Integer.valueOf(1);
list.remove(i);
List的遍历方式
- 迭代器遍历
Iterator it = list.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
- 增强for遍历
(变量s,其实就是一个第三方的变量而已,在循环过程中,依次遍历集合中的每个元素)
for (String s : list) {
System.out.println(s);
}
- lambda表达式遍历
list.forEach(s -> System.out.println(s));
- 普通for循环(有索引)
for(int i=0;i< list.size();i++){
System.out.println(list.get(i));
}
- 列表迭代器遍历
ListIterator
//额外添加了一个方法,在遍历的过程中可以通过迭代器添加元素
ListIterator<String> lt = list.listIterator();
while(lt.hasNext()){
System.out.println(lt.next());
}
遍历方式总结:
- 迭代器遍历:在遍历过程中需要删除元素,需要使用迭代器
- 列表迭代器遍历:在遍历过程中需要添加元素,使用列表迭代器
- 增强for遍历以及lambda表达式遍历:仅仅想遍历,不进行任何操作,可使用这两个
- 普通for:如果遍历的时候想操作索引
List实现类
ArrayList常用方法【有序可重复】
ArrayList集合底层原理
ArrayList底层是数组结构
利用空参创建的集合,在底层创建一个默认长度为0的数组
添加第一个元素时,底层会创建一个新的长度为10的数组 elementData
当数组添加满之后,会自动扩容为1.5倍
如果一次添加多个元素,1.5倍还放不下,则新创建的数组长度以实际为准
空参构造:
添加元素:
数组长度加一,把现有个数+1
当超出数组范围,就会进行自动扩容
在集合中不仅仅是一次添加一个元素,还可以一次添加多个元素
第一种情况:如果一次添加一个元素,那么进行数组扩容,并将原数组拷贝到扩容后的新数组中并返回即可
第二种情况:如果一次添加多个元素,假设100,此时数组需要扩容到100个单位才行
第三种情况:超出范围
LinkedList常用方法【有序可重复】
底层数据结构是链表
底层原理:
尾插法:
头插法:
迭代器的底层源码:
这个内部类就表示是ArrayList的迭代器,所以当我们调用多次这个方法的时候,那么就相当于是创建了多个迭代器的对象
拿着集合现在变化的次数,和创建对象时传递过来的次数进行比较
如何避免并发修改异常
在使用迭代器或者是增强for遍历集合的过程中,不要使用集合的方法去添加或者删除元素即可
泛型
JDK5引入的特性,可以在编译阶段约束操作的数据类型,并进行检查
没有泛型的时候,集合如何存储数据?
如果我们没有给集合指定类型,默认为所有的数据类型都是Object类型,此时可以给集合添加任意的数据类型,带来一个坏处:我们在获取数据的时候,无法使用它的特有行为【多态的弊端:不能访问子类的特有功能】
此时推出了泛型,泛型的好处:
- 统一数据类型
- 把运行时期的问题提前拿到了编译时期,避免了强制类型转换可能出现的异常,因为在编译阶段就能确定下来
Java中的泛型是伪泛型
泛型的细节:
- 泛型中不能写基本数据类型【基本数据类型是没有办法转化为Object类型,只能转换为包装类】
- 指定泛型的具体类型后,传递数据时,可以传入该类类型或者其子类类型【数据是有继承性的】
- 如果不写泛型,类型默认是Object
方法中形参类型不确定时:
- 使用类名后面定义的泛型【所有方法都能用】
- 在方法声明上定义自己的泛型【只有本方法能用】
泛型接口的两种使用方式:
- 实现类给出具体的类型
- 实现类延续泛型,创建实现类对象时再确定类型
泛型的继承
- 泛型不具备继承类型,但是数据具备继承性【泛型里面写的是什么类型,那么只能传递什么类型的数据】
利用泛型方法,有一个弊端:此时它可以接受任意的数据类型:Ye Fu Zi Student……
那么这个方法虽然不确定类型,但是希望以后只能传递 Ye Fu Zi
此时我们就可以使用泛型的通配符:
?表示不确定的类型 可以进行类型限定
? extends E:表示可以传递E或者E所有的子类类型
? super E:表示可以传递E或者E所有的父类类型
Student 跟这个继承结构没有关系,因此就会报错
应用场景:
- 如果我们在定义类、方法、接口的时候,如果类型不确定,就可以定义泛型类、泛型方法、泛型接口。
- 如果类型不确定,但是能直到以后只能传递某个继承体系中的,就可以使用泛型的通配符【限定泛型的类型】
红黑树
红黑树是一种特殊的二叉查找树,红黑树的每一个节点上都有存储位表示节点的颜色
每一个节点可以是红或者黑;红黑树不是高度平衡的,它的平衡是通过红黑规则进行实现的
为什么要发明红黑树?
平衡二叉树AVL:插入/删除很容易破坏“平衡”特性,需要频繁调整数的形态。例如插入操作导致不平衡,需要先计算平衡因子,找到最小不平衡树(时间开销较大),再进行左旋右旋调整。
红黑树RBT:插入/删除很多时候不会破坏“红黑”特性,无需频繁调整树的形态。即便需要调整,一般都可以在常数级时间内完成
红黑规则
- 每一个节点或是红色,或者是黑色的
- 根节点必须是黑色
- 如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为Nil,这些Nil视为叶节点,每个叶节点是黑色的
- 如果某一个节点是红色,那么它的子节点必须是黑色(不能出现两个红节点相连的情况)
- 对每一个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点
红黑添加节点的规则
- 添加节点默认是红色的(效率高)
Set集合
无序:存和取的顺序不一致
不重复:可以用来元素去重
无索引:没有带索引的方法,所以不能使用普通for循环遍历,也不能通过索引来获取元素
Collection中的三种遍历方式都适用【迭代器、增强for、Lambda表达式】
Set实现类的特点
- HashSet:无序、不可重复、无索引
- LinkedHashSet:有序、不可重复、无索引
- TreeSet:可排序、不可重复、无索引
HashSet集合
HashSet集合底层采用哈希表存取数据
哈希表是一种对于增删改查数据性能都比较好的结构
哈希表组成
JDK8之前:数组+链表
JDK8之后:数组+链表+红黑树
hashCode方法:
根据hashCode方法计算出int类型的整数
这个方法定义在Object类中,所有对象都可以调用,默认使用地址值进行计算【意义不大】
一般情况下,会重写hashCode方法,利用对象内部的属性值计算哈希值
如果没有重写hashCode方法,不同对象计算出的哈希值是不同的【地址值计算出来的】
如果已经重写hashCode方法,不同对象只要属性值相同,计算出的哈希值是一样的【属性值计算出来的】
在小部分情况下,不同的属性值或者不同的地址值计算出来的哈希值也有可能一样【哈希碰撞】
HashSet 底层原理:
JDK8以后,当链表长度超过8,而且数组长度大于等于64时,会自动转换为红黑树
集合中存储的是自定义对象时,必须要重写hashCode和equals方法
LinkedHashSet集合
有序、不重复、无索引
原理:底层数据结构依然是哈希表,只是每个元素又额外的多了一个双链表的机制记录存储的顺序
如果使用数据去重使用哪个?
默认使用HashSet
如果要求去重且存取有序,才使用LinkedHashSet
TreeSet集合
不可重复、无索引、可排序
可排序:按照元素的默认规则(由小到大)排序
TreeSet集合底层是基于红黑树的数据结构实现排序的,增删改查性能都较好
TreeSet的两种比较方式
1. 默认排序/自然排序:JavaBean类实现Comparable接口指定比较规则
实现接口,并重写抽象方法
this:表示当前要添加的元素
o:表示已经在红黑树中的元素
@Override
public int compareTo(Student o) {
//指定排序的规则:
//只看年龄 想要按照年龄升序进行排列
return this.getAge()-o.getAge();
}
public static void main(String[] args) throws ParseException {
TreeSet set = new TreeSet();
Student s1 = new Student("zhangsan",22);
Student s2 = new Student("lisi",18);
Student s3 = new Student("wangwu",30);
Student s4 = new Student("zhaoliu",10);
set.add(s1);
set.add(s2);
set.add(s3);
set.add(s4);
System.out.println(set);
}
2. 比较器排序:创建TreeSet对象的时候,传递比较器Comparator指定规则
例题:按照长度排序,如果一样长则按照首字母排序
public static void main(String[] args) throws ParseException {
TreeSet<String> set = new TreeSet<>(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.length()-o2.length()==0?o1.charAt(0)-o2.charAt(0) : o1.length()-o2.length();
}
});
set.add("cdfdgbfnbcv");
set.add("ab");
set.add("ef");
set.add("qwer");
System.out.println(set);
}
Map集合【双列集合】
双列集合的特点:
- 双列集合一次需要存一对数据,分别为键和值
- 键不能重复,值能重复
- 键和值是一一对应的
- 键+值 在Java中叫做Entry对象
Map是双列集合的顶层接口,全部的双列集合都可以继承使用
put方法的细节:
1、在添加数据的时候,如果键不存在,那么直接把键值对对象添加到map集合中
2、如果键是存在的,那么会把原有的键值对对象覆盖,会把被覆盖的值进行返回
map集合的遍历方式
1、键找值
//把map的key放到一个set集合当中
Set<String> keys = map.keySet();
//遍历集合
for(String key:keys){
System.out.println(key);
System.out.println(map.get(key));
}
2、键值对
//把map的键值对放到一个set集合当中
Set<Map.Entry<String, String>> entries = map.entrySet();
for(Map.Entry<String, String> entry:entries){
System.out.println(entry.getKey());
System.out.println(entry.getValue());
}
3、Lambda表达式
匿名内部类
map.forEach(new BiConsumer<String, String>() {
@Override
public void accept(String key, String value) {
System.out.println(key);
System.out.println(value);
}
});
Lambda表达式
map.forEach((key, value)-> System.out.println(key+value));
foreach方法的底层:
foreach其实就是利用键值对方式进行遍历,依次得到每一个键和值
HashMap集合
无序、不重复、无索引
transient Node<K,V>[] table;
哈希表结构中数组的名字
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
数组默认长度16
static final float DEFAULT_LOAD_FACTOR = 0.75f;
默认加载因子0.75
HashMap里面每一个对象包含以下内容:
链表中的键值对对象:
红黑树中的键值对对象:
除了键的哈希值、键、值等还有以下内容:
添加元素
添加元素的时候至少考虑三种情况:
2.1数组位置为null
2.2数组位置不为null,键重复,元素覆盖
2.3数组位置不为null,键不重复,元素挂在下面形成链表或者红黑树
Node<K,V>[] tab;
定义一个局部变量,用来记录哈希表中数组的地址值
Node<K,V> p;
临时的第三方变量,用来记录键值对对象的地址值
int n;
表示当前数组的长度
int i;
表示索引
1、如果是第一次添加数据,底层会创建一个默认长度为16,加载因子为0.75的数组
2、如果不是第一次添加数据,会看数组中元素是否达到了扩容条件
2.1如果没有达到扩容条件,底层不会做任何操作
2.2达到了扩容条件,底层会把数组扩容为原先的两倍,并把数据全部转移到新的哈希表中
3、把当前数组的长度赋值给n
添加第一个元素详细过程–数组位置为null
i = (n-1)&hash
拿着数组的长度跟键的哈希值进行计算,计算出当前键值对对象,在数组中应存入的位置
p=tab[i];
获取数组中对应元素的数组
if p==null
底层会创建一个键值对对象,直接放到数组当中
中间略去了else那段代码
threshold
记录的就是数组的长度*0.75,表示哈希表的扩容时机
return null;
表示当前没有覆盖任何元素,返回null
添加第一个元素详细过程—数组位置不为null
走上边略掉的那段else代码
p.hash == hash
数组中键值对的哈希值 当前要添加键值对的哈希值
给链表中挂节点:
大的else还没结束
【发生覆盖:】
如果e!=null :表示当前的键是一样的,值会被覆盖
else结束
LinkedHashMap集合
有序、不重复、无索引
TreeMap集合
可排序、不重复、无索引
TreeMap和TreeSet底层原理一样,都是红黑树结构的
可排序:对键进行排序
TreeMap中每一个节点的内部属性
TreeMap中空参构造
没有传递比较器对象
TreeMap中带参构造
传递了比较器对象
TreeMap添加第一个元素详细过程–root为null
TreeMap添加第二个元素详细过程–root不为null
不会进入根节点为null的判断,直接继续往后执行