目录
5.ArrayList、LinkedList、Vector三者的异同
Collection接口和Map接口是并列关系,
存储数据的特点不同。
学习要求:学完后要知道什么类型的数据适合什么结构去存储,选择合适的集合类去实现数据的保存,调用相关方法。不同集合类底层数据结构为何?如何实现数据的操作的:增删改查等
一、集合框架概述
1.理解集合
* 1.集合、数组都是对多个数据进行存储操作的结构,简称Java容器。 * 说明:此时的存储,主要指的是内存层面的存储,不涉及到持久化的存储(.txt,.jpg,.avi,数据库中) * //【持久化的存储:硬盘、服务器、数据库。讲到IO流时再讲】 * * 2.1 数组在存储多个数据方面的特点: * > 一旦初始化以后,其长度就确定了。 * > 数组一旦定义好,其元素的类型也就确定了。我们也就只能操作指定类型的数据了。 * //【严格是好处,放了不同类型就报错. * //集合类型不确定,什么都能往里放,并不好,不严格 * //泛型能解决这件事】 * 比如:String[] arr;int[] arr1;Object[] arr2; * //【Object[] arr2;可以多态】 * * 2.2 数组在存储多个数据方面的缺点: * > 一旦初始化以后,其长度就不可修改。 * > 数组中提供的方法非常有限,对于添加、删除、插入数据等操作,非常不便,同时效率不高。 * //【添加、删除、插入数据都要挪元素,没有方法可以直接拿来调用。数组只是数据结构中的一种,集合设计的数据结构很丰富:链表二叉树等,添加、删除、插入效率更高】 * * > 获取数组中实际元素的个数的需求,数组没有现成的属性或方法可用 * > 数组存储数据的特点:有序、可重复。对于无序、不可重复的需求,不能满足。
* 二、集合框架 * |----Collection接口:单列集合,用来存储一个一个的对象 * |----List接口:存储有序的、可重复的数据。 -->“动态”数组 * //【和数组特点很像】 * |----ArrayList、LinkedList、Vector * //【ArrayList、LinkedList、Vector的不同,底层扩容什么样的?】 * * |----Set接口:存储无序的、不可重复的数据 -->高中讲的“集合” * |----HashSet、LinkedHashSet、TreeSet * //【通常用Set做一些数据过滤的问题。有一些重复数据,放入Set里,放入顺序无所谓,发现放入的数据和前面的一样,就不要了,这样就过滤掉了】 * * |----Map接口:双列集合,用来存储一对(key - value)一对的数据 -->高中函数:y = f(x)//【x是key,y是value】 * |----HashMap、LinkedHashMap、TreeMap、Hashtable、Properties * //【HashMap的底层实现原理。jdk7和8不一样】
集合:Collection接口:
>List接口(动态数组):ArrayList、LinkedList、Vector
>Set接口(集合):HashSet、LinkedHashSet、TreeSet
Map接口(函数):
>HashMap、LinkedHashMap、TreeMap、Hashtable、Properties
开发中,集合中List、Map用的最多,Set很少用 * List用来替换数组
理解Map接口:
2.集合使用场景
二、Collection接口的常用方法
一共15个方法
1.常用方法1
public class CollectionTest {
@Test
public void test1(){
Collection coll = new ArrayList();
//add(Object e):将元素e添加到集合coll中
coll.add("AA");
coll.add("BB");
coll.add(123);//自动装箱
coll.add(new Date());
//size():获取添加的元素的个数
System.out.println(coll.size());//4
//addAll(Collection coll1):将coll1集合中的元素添加到当前的集合中
Collection coll1 = new ArrayList();
coll1.add(456);
coll1.add("CC");
coll.addAll(coll1);
System.out.println(coll.size());//6
System.out.println(coll);//【coll.toString()。ArrayList里重写的】
//clear():清空集合元素
//【不是把coll指向null,而是让元素都为null
// elementData[i] = null;】
//【coll.isEmpty()也不会报空指针异常】
coll.clear();
//isEmpty():判断当前集合是否为空
//【不是集合是否为null,而是判断集合中是否有元素,就是看size】
System.out.println(coll.isEmpty());
}
}
(1)add(Object e)
(2)addAll(Collection coll)
(3)size()
(4)clear() :
不是把coll指向null,而是让集合元素都为null
elementData[i] = null;
这样coll.isEmpty()也不会报空指针异常
(5)isEmpty():
不是集合是否为null,而是判断集合中是否有元素,就是看size是否为0
2.常用方法2
/* Collection接口中声明的方法的测试
*
* 结论:
* 向Collection接口的实现类的对象中添加数据obj时,要求obj所在类要重写equals().
*/
public class CollectionTest {
@Test
public void test1(){
Collection coll = new ArrayList();//【ArrayList,按顺序一个一个比较】
coll.add(123);//【123是Integer包装类,不是int基本数据类型】
coll.add(456);
/*
// Person p = new Person("Jerry",20);
// coll.add(p);
// System.out.println(coll.contains(p));//true
//【这样写,不管怎样都是true,
//用Object类的equals,contains内部调==比较对象的地址值,是true,p是引用,保存了地
址;
//重写了equals,调equals比较对象的内容也是true】
*/
coll.add(new Person("Jerry",20));
coll.add(new String("Tom"));
coll.add(false);//【false是Boolean包装类,不是boolean基本数据类型】
//1.contains(Object obj):判断当前集合中是否包含obj
//我们在判断时会调用obj对象所在类的equals()。
//【向Collection接口的实现类的对象中添加数据obj时,要求obj所在类要重写equals().】
boolean contains = coll.contains(123);
System.out.println(contains);
System.out.println(coll.contains(new String("Tom")));
//【结果是true,说明比较的是内容,用重写后的equals判断的,
// 不是用==判断的】
System.out.println(coll.contains(new Person("Jerry",20)));//false -->true
//【要重写Person的equals方法,否则用的是Object类的equals方法(==比地址值)】
//2.containsAll(Collection coll1):判断形参coll1中的所有元素是否都存在于当前集合中。
Collection coll1 = Arrays.asList(123,4567);
System.out.println(coll.containsAll(coll1));
}
@Test
public void test2(){
//3.remove(Object obj):从当前集合中移除obj元素。
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new Person("Jerry",20));
coll.add(new String("Tom"));
coll.add(false);
coll.remove(1234);
//【移除1234,先判断coll集合中有没有1234,所以还是要调equals】
System.out.println(coll);
coll.remove(new Person("Jerry",20));
//【重写了Person类的equals,所以可以删除成功】
System.out.println(coll);
//4. removeAll(Collection coll1):差集:从当前集合中移除coll1中所有的元素。
Collection coll1 = Arrays.asList(123,456);//【Arrays.asList(...)返回一个List集合】
coll.removeAll(coll1);
//【调了Person的equals方法】
System.out.println(coll);
}
@Test
public void test3(){
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new Person("Jerry",20));
coll.add(new String("Tom"));
coll.add(false);
//5.retainAll(Collection coll1):交集:获取当前集合和coll1集合的交集,并返回给当前集合
// Collection coll1 = Arrays.asList(123,456,789);
// coll.retainAll(coll1);
// System.out.println(coll);
//【remove和retainAll都会修改coll】
//6.equals(Object obj):要想返回true,需要当前集合和形参集合的元素都相同。
//【调equals方法的是集合coll,形参是一个Object对象,怎样返回true?
// Object obj也必须是集合,集合元素都和coll集合元素相同->true】
Collection coll1 = new ArrayList();
coll1.add(123);
coll1.add(456);
coll1.add(new Person("Jerry",20));
coll1.add(new String("Tom"));
coll1.add(false);
/*交换位置
coll1.add(456);
coll1.add(123);
coll1.add(new Person("Jerry",20));
coll1.add(new String("Tom"));
coll1.add(false);
System.out.println(coll.equals(coll1));
//【false,因为List是有序的,内容一样顺序不一样也不相同】
*/
System.out.println(coll.equals(coll1));//true
//【调用了Person的equals方法。因为输出了"Person equals()...."】
}
@Test
public void test4(){
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new Person("Jerry",20));
coll.add(new String("Tom"));
coll.add(false);
//7.hashCode():返回当前对象的哈希值
//【public native int hashCode();
// hashCode()定义在Object类中,所有对象都可以调用】
System.out.println(coll.hashCode());
//8.集合 --->数组:toArray()
//【 public Object[] toArray() {
// return Arrays.copyOf(elementData, size);
// }
// 返回值类型是Object[],
// 前面添加的123、456、new Person、new String、false都是Object类型的元素,所以返回值也是Object[],
// Object[]数组,表示数组元素都是Object类型的】
Object[] arr = coll.toArray();
for(int i = 0;i < arr.length;i++){
System.out.println(arr[i]);
}
//拓展:数组 --->集合:调用Arrays类的静态方法asList()
//【Arrays.asList(T...a),可变形参相当于数组】
List<String> list = Arrays.asList(new String[]{"AA", "BB", "CC"});
System.out.println(list);//【[AA, BB, CC]】
List<int[]> arr3 = Arrays.asList(new int[]{123, 456});
System.out.println(arr3);//【[[I@22927a81]】
//【[[I@22927a81],
// [I@22927a81一个元素,这个元素是一个一维的数组,这个数组的元素是int类型,
// 把new int[]{123, 456}数组整体当成一个元素,不是把数组内部的元素当成几个元素。
// 因为数组是引用数据类型,满足可变形参T...a(其实就是Object类),
//不会把123,456自动装箱。因为一整个int[]就可以看作是一个对象
// 所以整个数组被当作“数组这一个对象”,
// 元素是Integer类型,包装类,就可以单独当成一个对象。】
List arr4 = Arrays.asList(123, 456);//【删去泛型此时认为是两个元素】
System.out.println(arr4);//[123, 456]
System.out.println(arr4.size());//2
List arr1 = Arrays.asList(new int[]{123, 456});//【元素是基本数据类型,整个数组是一个对象】
System.out.println(arr1.size());//1
List arr2 = Arrays.asList(new Integer[]{123, 456});//【包装类】
System.out.println(arr2.size());//【2】
//9.iterator():返回Iterator接口的实例,用于【遍历】集合元素。放在IteratorTest.java中测试
}
}
//Person类
import java.util.Objects;
public class Person {
private String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public boolean equals(Object o) {
System.out.println("Person equals()....");//【测试语句,看contains()是否调用了equals】
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
常用方法二都会调用equals,所以要求元素所在类必须重写equals()
(6)contains(Object obj):
判断当前集合中是否包含obj 在判断时会调用obj对象所在类的equals()。 向Collection接口的实现类的对象中添加数据obj时,要求obj所在类要重写equals(),否则调用的是Object类的equals,比较地址值
(7)containsAll(Collection coll)
调用了元素所属类的equals()
(8)remove(Object obj)
调用了元素所属类的equals()
(9)removeAll(Collection coll)
调了元素所属类equals()
(10)retainAll(Collection coll)
调了元素所属类equals()
(11)equals(Object obj)
调了元素所属类的equals()
3.常用方法3
(12)hashCode() :定义在 Object类中
(13)toArray():集合-->数组
Object[] arr = coll.toArray();
(14)asList() :数组-->集合
List<String> list = Arrays.asList(new String[]{"AA", "BB", "CC"});
(15)iterator():
返回Iterator接口的实例,用于遍历集合元素
Collection要重写哪个方法? * equals() * List:equals() * Set:(HashSet、LinkedHashSet)equals()、hashCode() * (TreeSet)Comparable:compareTo() * Comparator:compare() * 【TreeSet调contains、remove还是要重写compare或compareTo方法而不是equals】
三、Iterator迭代器
1.使用Iterator遍历Collection
* 集合元素的遍历操作,使用迭代器Iterator接口 * //【只用来遍历Collection接口,Map接口不用迭代器遍历】 * 1.内部的方法:hasNext() 和 next() * 2.集合对象每次调用iterator()方法都得到一个全新的迭代器对象, * 默认游标都在集合的第一个元素之前。 * 3.内部定义了remove(),可以在遍历的时候,删除集合中的元素。此方法不同于集合直接调用remove() * //【这是迭代器里的remove(),不是Collection集合中的remove()】
public class IteratorTest {
@Test
public void test1(){
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new Person("Jerry",20));
coll.add(new String("Tom"));
coll.add(false);
Iterator iterator = coll.iterator();//【Iterator是接口,叫迭代器接口】
//方式一:
// System.out.println(iterator.next());
// System.out.println(iterator.next());
// System.out.println(iterator.next());
// System.out.println(iterator.next());
// System.out.println(iterator.next());
// System.out.println(iterator.next());//报异常:NoSuchElementException【没有这个元素】
//方式二:不推荐
// for(int i = 0;i < coll.size();i++){
// System.out.println(iterator.next());
// }
//方式三:推荐
hasNext():判断是否还有下一个元素
while(iterator.hasNext()){
//next():①指针下移 ②将下移以后集合位置上的元素返回
System.out.println(iterator.next());
}
}
//【遍历集合的错误写法】
@Test
public void test2(){
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new Person("Jerry",20));
coll.add(new String("Tom"));
coll.add(false);
//错误方式一:
// Iterator iterator = coll.iterator();
// while((iterator.next()) != null){
// System.out.println(iterator.next());
// //【输出:
// // 456
// // Tom】
// }
//错误方式二:
//集合对象每次调用iterator()方法都得到一个【全新的迭代器对象】,默认游标都在集合的第一个元素之前。
while (coll.iterator().hasNext()){
System.out.println(coll.iterator().next());
//【输出:
// 123
// 123
// ...
// 123】
}
}
//测试Iterator中的remove()
//如果还未调用next()或在上一次调用 next 方法之后已经调用了 remove 方法,
// 再调用remove都会报IllegalStateException。
@Test
public void test3(){
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new Person("Jerry",20));
coll.add(new String("Tom"));
coll.add(false);
//删除集合中"Tom"
Iterator iterator = coll.iterator();
while (iterator.hasNext()){
// iterator.remove();//【异常】
Object obj = iterator.next();//【add时都是认为添加的是Object类型,取的时候也是Object类型】
if("Tom".equals(obj)){//【"Tom".equals(obj)比obj.equals("Tom")健壮性更好,以防obj是null的情况】
iterator.remove();
// iterator.remove();
}
}
//遍历集合
iterator = coll.iterator();
//【再次遍历要重新获取iterator指针,因为删除时用的迭代器已经指向集合中最后一个元素了】
while (iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
2.Iterator迭代器的执行原理
3.Iterator迭代器的两种错误写法
见1
4.Iterator迭代器remove()的使用
见1
四、新特性foreach循环遍历集合
jdk 5.0 新增了foreach循环,用于遍历Collection集合、数组
for(集合元素的类型 局部变量 : 集合对象)遍历集合 for(数组元素的类型 局部变量 : 数组对象)遍历数组 【foreach内部仍然调用了迭代器。】 【Object这个位置,由coll集合中的元素是什么类型决定】 【Object obj就像for循环里的int i一样】 for(Object obj : coll){ System.out.println(obj); } /*【foreach的工作过程: coll取第一个元素赋给Object obj,然后sout打印obj coll取第二个元素赋给Object obj,然后sout打印obj ... coll取最后一个元素赋给Object obj,然后sout打印obj 怎么取的? debug看一下 内部仍然调用了迭代器】 */
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;
/**
* jdk 5.0 新增了foreach循环,用于遍历Collection集合、数组
*
* @author shkstart
* @create 2019 上午 11:24
*/
public class ForTest {
@Test
public void test1(){
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new Person("Jerry",20));
coll.add(new String("Tom"));
coll.add(false);
//for(集合元素的类型 局部变量 : 集合对象)遍历集合
//for(数组元素的类型 局部变量 : 数组对象)遍历数组
//【内部仍然调用了迭代器。】
//【Object这个位置,由coll集合中的元素是什么类型决定】
//【Object obj就像for循环里的int i一样】
for(Object obj : coll){
System.out.println(obj);
}
/*【foreach的工作过程:
coll取第一个元素赋给Object obj,然后sout打印obj
coll取第二个元素赋给Object obj,然后sout打印obj
...
coll取最后一个元素赋给Object obj,然后sout打印obj
怎么取的?
debug看一下
内部仍然调用了迭代器】
*/
}
@Test
public void test2(){
int[] arr = new int[]{1,2,3,4,5,6};
//for(数组元素的类型 局部变量 : 数组对象)
for(int i : arr){
System.out.println(i);
}
}
//练习题
@Test
public void test3(){
String[] arr = new String[]{"MM","MM","MM"};
// //方式一:普通for赋值
// for(int i = 0;i < arr.length;i++){
// arr[i] = "GG";
// }
//方式二:增强for循环
for(String s : arr){
s = "GG";
}
//测试
// for(String s : arr){
System.out.println("前" + s);
// s = "GG";
System.out.println("后" + s);
// }
for(int i = 0;i < arr.length;i++){
System.out.println(arr[i]);
}
}
}
五、List接口
1.List接口常用实现类的对比
* 1. List接口框架 * //单列数据 * |----Collection接口:单列集合,用来存储一个一个的对象 * |----List接口:存储有序的、可重复的数据。 -->“动态”数组,替换原有的数组 * |----ArrayList:作为List接口的主要实现类;线程不安全的,效率高;底层使用Object[] elementData存储 * //【基本上用这个,底层依然是数组,顺序存储。Array->数组】 * //【超了长度,不用自己扩容,底层已经把方法写好了】 * //【多线程,即使ArrayList是不安全的,也不用Vector,怎么解决呢? * //后面Collections工具类,List synchronizedList(List<T> list), * //把ArrayList当作实参传进去,就可以返回一个线程安全的集合】 *
* |----LinkedList:【对于频繁的插入、删除操作,使用此类效率比ArrayList高】; 底层使用双向链表存储 * //【Link-->链表】 * //【底层存储的数据结构不同,对数组元素的操作就不同】
* * |----Vector:作为List接口的古老实现类;线程安全的,效率低;底层使用Object[] elementData存储 * //效率低,所以不用。synchronized * *
2.ArrayList的源码分析
* 2.1 jdk 7情况下 * ArrayList list = new ArrayList();//【底层创建了长度是10的Object[]数组elementData】 * //【回顾:StringBuffer底层创建了一个长16的char[] value】 * * list.add(123);//elementData[0] = new Integer(123); * ... * list.add(11);//如果此次的添加导致底层elementData数组容量不够,则扩容。 * 默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中。 * * 【结论:建议开发中使用带参的构造器:ArrayList list = new ArrayList(int capacity)】 * //【避免中间环节扩容,效率高】
* 2.2 jdk 8中ArrayList的变化: * ArrayList list = new ArrayList();//底层Object[] elementData初始化为{}.并没有创建长度为10的数组 * * list.add(123);//【第一次调用add()时,底层才创建了长度10的数组】,并将数据123添加到elementData[0] * ... * 后续的添加和扩容操作与jdk 7 无异。
* 2.3 小结:jdk7中的ArrayList的对象的创建类似于单例的【饿汉式】,而jdk8中的ArrayList的对象的创建类似于【单例的懒汉式】,【延迟了数组的创建,节省内存】。 * //【jdk7,调构造器时候,ArrayList就在底层创建了长度是10的数组 * //jdk8,调构造器时,创建了数组={},add的时候才让数组的长度变为10】 * //【jdk8的更好。例如:后台造了个ArrayList,要往里面放数据,数据要在数据库中请求,请求需要时间,7已经造好了,8不提前造,节省内存】
3.LinkedList的源码分析
//jkd7、jkd8无区别 * LinkedList list = new LinkedList(); 内部声明了Node类型的first和last属性, 默认值为null * list.add(123);//将123封装到Node中,创建了Node对象。 * * 其中,Node定义为:体现了LinkedList的双向链表的说法 * 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; } }
4.Vector的源码分析
* jdk7和jdk8中通过Vector()构造器创建对象时,底层都创建了长度为10的数组。 * 在扩容方面,默认扩容为原来的数组长度的2倍。
5.ArrayList、LinkedList、Vector三者的异同
* 同:三个类都是实现了List接口,存储数据的特点相同:存储有序的、可重复的数据 * 不同:见上
* ArrayList和Vector比 * ArrayList和LinkedList比: * ArrayList LinkedList * 查找 O(1) O(n) * 删除 O(n) O(1) * 插入 O(n) O(1)
6.List接口中的常用方法
void add(int index, Object ele):在index位置插入ele元素 boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来 Object get(int index):获取指定index位置的元素 int indexOf(Object obj):返回obj在集合中首次出现的位置 int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置 Object remove(int index):移除指定index位置的元素,并返回此元素 //【重载Collection接口中的remove(Object obj)。remove可以按照索引删,也可以按照对象删】 Object set(int index, Object ele):设置指定index位置的元素为ele List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的子集合 //【用数组的地方都可以替换成ArrayList。数组里定义的total相当于ArrayList里的size】 以上的方法都是基于index索引才有的 总结:常用方法: 增:add(Object obj) //往末尾添加 删:remove(int index) / remove(Object obj) 改:set(int index, Object ele) 查:get(int index) 插:add(int index, Object ele) 长度:size() 遍历:① Iterator迭代器方式 ② 增强for循环 ③ 普通的循环 //【因为有index索引了,所以也可以用普通的for循环】
public class ListTest {
@Test
public void test3(){
ArrayList list = new ArrayList();
list.add(123);
list.add(456);
list.add("AA");
//方式一:Iterator迭代器方式
Iterator iterator = list.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
System.out.println("***************");
//方式二:增强for循环
for(Object obj : list){
System.out.println(obj);
}
System.out.println("***************");
//方式三:普通for循环
for(int i = 0;i < list.size();i++){
System.out.println(list.get(i));//get(index)方法
}
}
@Test
public void test2(){
ArrayList list = new ArrayList();
list.add(123);
list.add(456);
list.add("AA");
list.add(new Person("Tom",12));
list.add(456);
//int indexOf(Object obj):返回obj在集合中首次出现的位置。如果不存在,返回-1.
//【java中index中找不到都返回-1】
int index = list.indexOf(4567);
System.out.println(index);
//int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置。如果不存在,返回-1.
System.out.println(list.lastIndexOf(456));
//Object remove(int index):移除指定index位置的元素,并返回此元素
//【ArrayList底层是数组,删除后,要把后面的元素往前移】
Object obj = list.remove(0);
System.out.println(obj);
System.out.println(list);
//Object set(int index, Object ele):设置指定index位置的元素为ele
list.set(1,"CC");
System.out.println(list);
//List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的【左闭右开】区间的子集合
List subList = list.subList(2, 4);
System.out.println(subList);
System.out.println(list);
}
@Test
public void test1(){
ArrayList list = new ArrayList();
list.add(123);
list.add(456);
list.add("AA");
list.add(new Person("Tom",12));
list.add(456);
System.out.println(list);
//void add(int index, Object ele):在index位置插入ele元素
//【ArrayList底层是数组,添加前,要把索引后的元素都往后挪,再添加】
list.add(1,"BB");
System.out.println(list);
//boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来
List list1 = Arrays.asList(1, 2, 3);//【数组转集合】
list.addAll(list1);
// list.add(list1);
//【add(list1)会把list1整体当成一个元素加入list,size = 6+1 = 7。集合类也是Object类】
System.out.println(list.size());//9 = 6+3
//Object get(int index):获取指定index位置的元素
System.out.println(list.get(0));
}
}
7.List遍历及方法总结
见6,test3
迭代器、foreach(增强for循环)、普通for循环
六、Set接口
1.Set接口实现类的对比
* 1. Set接口的框架: * * |----Collection接口:单列集合,用来存储一个一个的对象 * |----Set接口:存储无序的、不可重复的数据 -->高中讲的“集合” * |----HashSet:作为Set接口的主要实现类;线程不安全的;可以存储null值 * |----LinkedHashSet:作为HashSet的子类;遍历其内部数据时,可以按照添加的 顺序遍历 * //【这不叫有序,那什么是无序呢?】 * 对于频繁的遍历操作,LinkedHashSet效率高于HashSet. * //【为频繁操作遍历准备的类】 * * //【LinkedHashSet在HashSet的基础上加了前后的指针,或者说是链表, * 效果是看起来"有序",使得遍历LinkedHashSet的元素时,是按照添加时的顺序来的,就因为加了前后指针。但这个有序是骗人的,因为Set是无序的】 * * |----TreeSet:可以按照添加对象的指定属性,进行排序。 * //底层用二叉树(红黑树)存的 * //【之前List的实现类和HashSet类都可以随便放不同类型的对象,123,new Person,"haha"】 * //要求可以排序,所以放入TreeSet的数据必须是同一个类new的。 * //然后可以按照对象的某些属性排序。 * //【Comparable、Comparator,然后调Arrays.sort或Collections.sort * //这两个接口在TreeSet体现】 * * //【为排序做准备的类】 * * * //【开发中用List和Map比较多,用Set比较少。Set和后面讲的Map有关,Map讲源码,Set自然懂了】 * * 1. Set接口中没有额外定义新的方法,使用的都是Collection中声明过的方法。 * //【List有序,有index索引,增加了很多和索引相关的方法。Set无序,没索引,所以没有新的方法】 * * 2. 要求:向Set(主要指:HashSet、LinkedHashSet)中添加的数据,其所在的类一定要重写hashCode()和equals() * 要求:重写的hashCode()和equals()尽可能保持一致性:相等的对象必须具有相等的散列码 * //【怎么保持一致性?在hashCode()用到的属性,也要在equals中用】 * 重写两个方法的小技巧:对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值。直接alt+shift+s生成最保险最方便
2.Set的无序性和不可重复性的理解
见3,元素添加过程。
以HashSet为例说明: 1. 无序性:不等于随机性。存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定的。 //无序性并不是指遍历输出和添加时的顺序不同,以HashSet为例, //【哈希值:hashCode()方法,Object类中的方法】 2. 不可重复性:保证添加的元素按照equals()判断时,不能返回true.即:相同的元素只能添加一个。 //【记得重写equals,和hashCode,仅重写equals,还是判断地址值是否相同】 //【用Set过滤重复数据,用的就是不可重复性】
3.HashSet中元素的添加过程
添加元素的过程:以HashSet为例: 从添加元素的过程理解Set的无序性和不可重复性 //【这个理解有助于后面理解HashMap】 //【TreeSet和HashSet的添加元素过程不一样】
//很容易想到的方法:添加第一个元素,不用equals,因为是空的
//添加第二个元素,和第一个元素equals,false就放入HashSet容器里,true就不放入
//添加第三个元素,和第一个、第二个元素都要equals,false就放入,
//...
//添加第一千个元素(【底层是数组,也会扩容】),和前面999个元素equals,....
//这样效率很低,数越多,要遍历equals的次数也就越多,不靠谱。
//Set不是这样做的, //7中,一创建Set,数组长度就指定为16了,8中,没有指定。 //它先考虑哈希值,调用所属类的hashCode()算哈希值(所以不止要重写equals还要重写hashCode()),用对象的属性算 //比如哈希值是13462,这个数就决定了对象在数组中的位置,不可能13462是索引,数组没那么长,所以对哈希值操作,算出真正的位置在哪 //假设操作的算法是和16取模,13462 % 16 = 0 ~ 15,取模结果就作为对象存在数组里的位置。 //Set的真实做法: //1.添加第一个元素,算出哈希值,算出位置值,不用equals //2.添加第二个元素,算出哈希值,算出位置值,先看位置上有没有元素,没有就直接放上去,不用equals //3.添加第三个元素,算出哈希值,算出位置值,先看位置上有没有元素,没有就直接放上去,不用equals //【不用equals比较,这就要求重写hashCode()和equals()时要一致】 //4.添加第四个元素,算出哈希值,算出位置值,先看位置上有没有元素,有元素(比如是第三个元素), //此时,第三个元素,第四个元素,哈希值不一定一样,只是根据算法算出的位置值一样。比如1 % 16 = 1,17 % 16 = 1。 //如果哈希值不一样,则认为是不同的元素,就不用equals。 //那第四个元素放在哪?以链表的方式存放第三个和第四个元素。 //5.添加第五个元素,算出哈希值,算出位置值,先看位置上有没有元素,有元素(比如是第一个元素), //而第五个元素和第一个元素的哈希值一样,不能直接说这两个元素一样,此时要用equals判断, //用第五个元素的equals判断,把第一个元素作为实参传进去,第五个元素.equals(第一个元素), //返回值是true,第五个元素就放不进容器了(哈希值一样,equals也一样), //返回值是false,第五个元素就放进容器了(哈希值一样,但equals不一样),添加成功
//【上面这样做的好处: //假设现在放第一千个元素:算哈希值,算位置值,位置上没有元素就直接放入,比自己想的方法高效,一个都不用比。 //位置上有元素(此时可能不止一个元素),再长也不会有999个,一个个比完,都不一样,那就存入, //和某一个一样,那后面的就不用比了,添加失败】
视频总结的: 我们向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值, 此哈希值接着通过某种算法计算出在HashSet底层数组中的存放位置(即为:索引位置),判断 数组此位置上是否已经有元素: 如果此位置上没有其他元素,则元素a添加成功。 --->添加成功情况1 如果此位置上有其他元素b(或【以链表形式存在的多个元素】),则比较元素a与元素b的hash值: 如果hash值不相同,则元素a添加成功。--->添加成功情况2 如果hash值相同,进而需要调用元素a所在类的equals()方法: //【将元素b或以链表形式存在的多个元素通过循环的方式,往equals形参里放】 equals()返回true,元素a添加失败 equals()返回false,则元素a添加成功。--->添加成功情况3 对于添加成功的情况2和情况3而言:元素a 与已经存在指定索引位置上数据以链表的方式存储。 jdk 7 :元素a放到数组中,指向原来的元素。 jdk 8 :原来的元素在数组中,指向元素a 总结:七上八下
【HashSet底层:数组+链表的结构。】
//【添加元素过程:哈希值,算位置值->比哈希值->调equals】
//【以上的对讲HashMap非常有用,因为new HashSet(),其实new了HashMap() // public HashSet() { // map = new HashMap<>(); // } // //add添加元素,添加到HashMap中了 // public boolean add(E e) { // return map.put(e, PRESENT)==null; // }】
* HashMap添加元素过程和HashSet一样。Set底层就是Map * * * Set : HashSet LinkedHashSet TreeSet * Map : HashMap LinkedHashMap TreeMap * new一个HashSet,底层new的是HashMap * new一个LinkedHashSet,底层new的是LinkedHashMap * new一个TreeSet,底层是TreeMap
Set的无序性理解:
Set的无序性和不可重复性理解:
public class SetTest {
@Test
public void test1(){
Set set = new HashSet();
set.add(456);//【添加第一个元素不用比较】
set.add(123);
set.add(123);
set.add("AA");
set.add("CC");
set.add(new User("Tom",12));
set.add(new User("Tom",12));
//【没重写User的equals和hashCode,遍历时就会有两个Tom,12
// 没重写hashCode,调的是Object类里的hashCode,
// public native int hashCode(); native说明调用的是底层的c语言。
// 可以看作是随机计算哈希值,所以两个User对象哈希值不同,
// 那算的位置值就不同,第二个User对象就直接放到一个空位置,比都不用比,所以连equals都没调,没输出"User equals()...."】
set.add(129);
Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
// Set set = new HashSet();
// set.add(456);
// set.add(123);
// set.add("AA");
// set.add("CC");
// set.add(new User("Tom",12));
// set.add(129);
//
// Iterator iterator = set.iterator();
// while(iterator.hasNext()){
// System.out.println(iterator.next());
// }
/*
AA
CC
129
456
123
User{name='Tom', age=12}
输出和加入的顺序不同。【但这不叫无序性,LinkedHashSet遍历和添加时的顺序一样,能说是有序的吗?不能】
无序性不是随机性
*/
}
//LinkedHashSet的使用
//LinkedHashSet作为HashSet的子类,在添加数据的同时,每个数据还维护了两个引用,记录此数据前一个
//数据和后一个数据。
//优点:对于频繁的遍历操作,LinkedHashSet效率高于HashSet
@Test
public void test2(){
Set set = new LinkedHashSet();
set.add(456);
set.add(123);
set.add(123);
set.add("AA");
set.add("CC");
set.add(new User("Tom",12));
set.add(new User("Tom",12));
set.add(129);
Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
/*
User equals()....
456
123
AA
CC
User{name='Tom', age=12}
129
// 【遍历和添加时的顺序一样,能说LinkedHashSet是有序的吗?不能。(Set是无序、不可重复的)
//无序性:指存放到数组位置上,是无序的,不是按顺序一个一个挨着放的。
//那凭什么LinkedHashSet可以按照添加的顺序遍历?因为有Linked,有链表。课件p40】
*/
}
}
public class User implements Comparable{
private String name;
private int age;
public User() {
}
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public boolean equals(Object o) {
System.out.println("User equals()....");//【Set:不可重复性。测试Set类的iterator有无调用equals方法】
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
if (age != user.age) return false;
return name != null ? name.equals(user.name) : user.name == null;
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
//【String重写过hashCode(),不再是随机计算了】
result = 31 * result + age;
return result;
}
//return name.hashCode() + age;//【两个对象属性值不一样,哈希值也别一样;属性值一样,则哈希值一样】
//【用这句话的坏处:
// 对象a:name的哈希值24,age等于20;对象b:name的哈希值是20,age等于24.两个的属性值不同,
// 但是最后return的值相同,调equals->false。指针连一下。
// 也存上去了,但是觉得能少用指针就少用指针
//
// result = 31 * result + age;让result乘上一个数再加上age,这样最后结果相同的概率就比较低。
// 为什么是31?2^5 - 1 = 32
// 2 << 5 - 1】
//2.按照姓名从大到小排列--->二级排序:按照姓名从大到小排列,年龄从小到大排列
@Override
public int compareTo(Object o) {
if (o instanceof User){
User user = (User) o;
// return -this.name.compareTo(user.name);
int compare = -this.name.compareTo(user.name);//【保存compareTo的返回值】
if (compare != 0){
return compare;
}else {
return Integer.compare(this.age,user.age);//【this.age是int基本数据类型,没有方法】
}
}else {
throw new RuntimeException("输入的类型不匹配");
}
}
// //1.按照姓名从大到小排列
// @Override
// public int compareTo(Object o) {
// if (o instanceof User){
// User user = (User) o;
// return -this.name.compareTo(user.name);
// }else {
// throw new RuntimeException("输入的类型不匹配");
// }
// }
}
4.关于hashCode()和equals()的重写
见3
5.LinkedHashSet的使用
见3
6.TreeSet的自然排序和定制排序
TreeSet的底层:
TreeSet和后面要讲的TreeMap采用红黑树的存储结构
1.向TreeSet中添加的数据,要求是相同类的对象。 2.两种排序方式:自然排序(实现Comparable接口) 和 定制排序(Comparator) 3.自然排序中,比较两个对象是否相同的标准为:compareTo()返回0。不再调用equals(). 4.定制排序中,比较两个对象是否相同的标准为:compare()返回0。不再是调用equals().
import org.junit.Test;
import java.util.Comparator;
import java.util.Iterator;
import java.util.TreeSet;
public class TreeSetTest {
@Test
public void test1(){
TreeSet set = new TreeSet();
//失败:不能添加【不同类】的对象
// set.add(123);
// set.add(456);
// set.add("AA");
// set.add(new User("Tom",12));
//举例一:
// set.add(34);//【都是Integer类型】
// set.add(-34);
// set.add(43);
// set.add(11);
// set.add(8);
/*
-34
8
11
34
43
【按从小到大遍历】
//【包装类实现了Comparable接口,按照从小到大的顺序比较】
*/
//举例二:
// set.add(new User("Tom",12));
// set.add(new User("Jerry",32));
// set.add(new User("Jim",2));
// set.add(new User("Mike",65));
// set.add(new User("Jack",33));
//【User没实现Comparable,没重写compareTo时,遍历报异常
// java.lang.ClassCastException: com.atguigu.java1.User cannot be cast to java.lang.Comparable
// 也就是说,TreeSet的iterator遍历调用了Comparable的compareTo,或Comparator的compare】
//【举例三:】
set.add(new User("Tom",12));
set.add(new User("Jerry",32));
set.add(new User("Jim",2));
set.add(new User("Mike",65));
set.add(new User("Jack",33));
set.add(new User("Jack",56));
//【遍历结果:
/*
User{name='Tom', age=12}
User{name='Mike', age=65}
User{name='Jim', age=2}
User{name='Jerry', age=32}
User{name='Jack', age=33}
*/
// 按照Set的添加过程,new User("Jack",33)和new User("Jack",56),是不一样的两个元素,
// 但compareTo 比较这两个对象,是一样的,return了0】
//【Set里其他的实现类(HashSet和LinkedHashSet)都是用equals比较是否相同,
// TreeSet用compareTo比较元素是否相同,所以要完善仅用姓名比较的compareTo,
// 二级排序】
Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
@Test
public void test2(){
Comparator com = new Comparator() {
//按照年龄从小到大排列
@Override
public int compare(Object o1, Object o2) {
if(o1 instanceof User && o2 instanceof User){
User u1 = (User)o1;
User u2 = (User)o2;
return Integer.compare(u1.getAge(),u2.getAge());
}else{
throw new RuntimeException("输入的数据类型不匹配");
}
}
};
TreeSet set = new TreeSet(com);
//【没传Comparator,就把元素用从小到大的逻辑(实现Comparable)放进TreeSet的底层结构红黑树中,
// 传了Comparator,就用自己写的Comparator逻辑add元素】
set.add(new User("Tom",12));
set.add(new User("Jerry",32));
set.add(new User("Jim",2));
set.add(new User("Mike",65));
set.add(new User("Mary",33));
set.add(new User("Jack",33));
set.add(new User("Jack",56));
Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
}
}
七、Map接口
1.Map接口及其多个实现类的对比
* 一、Map的实现类的结构: * |----Map:双列数据,存储key-value对的数据 ---类似于高中的函数:y = f(x) * |----HashMap:作为Map的主要实现类;线程不安全的,效率高;存储null的key和value * //【和ArrayList一样,即使涉及到多线程问题,也不会因为线程安全而用Hashtable, * 而是用Collections工具类中的相关方法,把HashMap变成安全的】 * * |----LinkedHashMap:保证在遍历map元素时,可以按照添加的顺序实现遍历。 * 原因:在原有的HashMap底层结构基础上,添加了一对指针,指向前一个和 后一个元素。 * 对于频繁的遍历操作,此类执行效率高于HashMap。 * * |----TreeMap:保证按照添加的key-value对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序 * //排序是按照key来排的 * 底层使用红黑树 * //和HashMap、LinkedHashMap、Hashtable底层结构不同 * * |----Hashtable:作为古老的实现类;线程安全的,效率低;不能存储null的key和value * |----Properties:常用来处理配置文件。key和value都是String类型
* //出现时间:Hashtable存键值对 -> Map -> HashMap -> TreeMap存有序键值对 * HashMap遍历效率太差 -> LinkedHashMap * //Vector -> List -> ArrayList -> LinkedList * //Set -> HashSet -> TreeSet * -> LinkedHashSet * * * HashMap的底层:数组+链表 (jdk7及之前) * 数组+链表+红黑树 (jdk 8)
* 面试题:
* 1. HashMap的底层实现原理?
* 2. HashMap 和 Hashtable的异同?
* 3. CurrentHashMap 与 Hashtable的异同?(暂时不讲)
* //不论如何,把HashMap用工具类转成安全的之后,同步代码块里面都是单线程的,
* 为了让高并发的时候,执行效率更高,引入CurrentHashMap
transient讲序列化的时候再说
2.对Map中key、value的理解
* //对Map的key、value的理解: * Map中的key:无序的、不可重复的,使用Set存储所有的key ---> key所在的类要重写equals()和hashCode() (以HashMap为例) * //用什么Set存,取决于是什么Map:HashMap就用HashSet存,LinkedHashMap就用LinkedHashSet存 * //自定义类重写equals()和hashCode()保证存入的数据是不可重复的 * //如果是TreeMap,就要重写compareTo或compare保证数据不重复 * * Map中的value:无序的、可重复的,使用Collection存储所有的value --->value所在的类要重写equals() * 一个键值对:key-value构成了一个entry对象。 * //【key、value可以看作是是对象entry的属性】 * * Map中的Entry:无序的、不可重复的,使用Set存储所有的Entry
3.HashMap在jdk7中的底层实现原理
HashMap的底层实现原理?以jdk7为例说明: * HashMap map = new HashMap(): * 在实例化以后,底层创建了长度是16的一维数组Entry[] table。//put的数据都放在数组中 * ...可能已经执行过多次put... * map.put(key1,value1)://【把key和value放入数组中,怎么放?以下是过程】 * 首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。
* 如果此位置上的数据为空,此时的key1-value1添加成功。 ----情况1 * //实质上是Entry1添加成功 * * 如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值: * 如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。----情况2 * //要和链表上所有的entry比哈希值 * 如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用key1所在类的equals(key2)方法,比较: * 如果equals()返回false:此时key1-value1添加成功。----情况3 * 如果equals()返回true:使用value1替换value2。 * //【put("Tom",23); key是Tom,value是23 * //put("Tom",45); * //"Tom"和"Tom"的哈希值相同,equals返回true,遍历map结果是Tom 45】
* //【总结:1.算哈希值 --> 算位置值 * //2.位置为null,直接添加 * //3.位置不为null,比哈希值 --> 3.1哈希值不同,添加为链表 * --> 3.2哈希值相同,(比地址值)比equals,equals返回false,添加为链表,equals返回true,替换】
* 补充:关于情况2和情况3:此时key1-value1和原来的数据以【链表】的方式存储。 * //【jdk7源码:】 * //【void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); }//【这一块逻辑体现的是扩容】 createEntry(hash, key, value, bucketIndex);//【不扩容,就直接添加】 } * * void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex];//【①先取出位置上原有的元素,】 table[bucketIndex] = new Entry<>(hash, key, value, e); //【②new新的Entry,放新添加的元素】 //【④把新造的Entry放入位置上】 size++; } Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n;//【③原有的元素作为当前对象的next】 key = k; hash = h; } 】
* //【jdk8源码:】 * //【public V put(K key, V value) { return putVal(hash(key), key, value, false, true); // } //【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) //【首次put的逻辑】 //【数组为空,就帮忙造一个数组】 // 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; // if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) //【比哈希值:哈希值相等,比地址值,地址值一样就不用比equals了,比equals,返回true】 //【这里只是比了位置上第一个元素的哈希值】 // e = p;//【把p存到e里,还没有替换】 //【能到这来,说明和位置上第一个元素哈希值不等或者哈希值相等equals返回false】 //【还要和链表上其他元素比较】 // else if (p instanceof TreeNode) // e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // else { // for (int binCount = 0; ; ++binCount) { //【for循环:位置上可能不止一个元素,可能是链表,所以要和链表上所有的元素比】 // if ((e = p.next) == null) {//【p.next == null,说明这个位置上只有一个元素p】 // p.next = newNode(hash, key, value, null);//【把新造的key-value作为p的next,这就是七上八下的八下】 // if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st //【这一块是涉及什么时候链表变成红黑树】 //【当添加的链表长度超过8时,】 // treeifyBin(tab, hash);//【就把链表变成红黑树】 // break; // } //【p.next不为null,说明p后面还有元素】 //【和链表上所有的Node比较哈希值,比较地址值,比较equals】 // if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) // break;//【哈希值一样,equals一样,就break跳出循环,去下面替换】 // p = e;//【就可以变成下一个元素,循环继续,继续往下进行比较】 // } // } //【替换】 // 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 void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize();//【数组长度<64,不把链表变成树,而是扩容数组,因为树结构还是比数组复杂的,直接扩容更方便】 else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) hd.treeify(tab); } } *
* 在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置【非空】)时,扩容。
* 默认的扩容方式:扩容为原来容量的【2倍】,并将原有的数据【复制】过来。
* //【扩容完后,重新计算在新数组中的位置值,有可能原来是链表,重新计算后就不在同一个位置了】
*
* 【jdk8 相较于jdk7在底层实现方面的不同:】
* 1. new HashMap():底层没有创建一个长度为16的数组
* 2. jdk 8底层的数组是:Node[],而非Entry[]
* 3. 首次调用put()方法时,底层创建长度为16的数组
* 4. jdk7底层结构只有:数组+链表。jdk8中底层结构:数组+链表+【红黑树】。
* 4.1 形成链表时,七上八下(jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素)
4.2 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,此时此索引位置上的所数据改为使用红黑树存储。
*
* DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16
* DEFAULT_LOAD_FACTOR:HashMap的默认加载因子:0.75
* //【ArrayList底层一开始容量是10,存前十个元素都不会扩容,到第11个才扩容
* //HashMap底层一开始是16,不是到第17个才扩容,而是【提前扩容】
* //为什么要提前扩容,不能满了再扩吗?它一定会满吗?怎样才叫满?
* //因为位置值是通过哈希值算的,而且还有链表,有些位置可能始终都不会有元素。
* //提前扩容:让数组里出现链表的情况尽可能少
* //想让链表出现的少,让加载因子尽可能小
* //假设加载因子=0.3,16x0.3=4.8,超过4个就要扩容,对数组的利用率就低了,所以太小也不好,
* //太大了,链表就多了,也不太好。
* //大量测试0.75兼并了对数组利用率高+链表少】
*
* threshold:扩容的临界值,=容量*填充因子:16 * 0.75 => 12
* //【扩容和threshold有关,扩容不是等到快装满了才扩容】
*
* TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8
* MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64
4.HashMap在jdk8中的底层实现原理
见3
jdk7:数组+链表
jdk8:数组+链表+红黑树
5.HashMap在jdk7中的源码分析
见3
6.HashMap在jdk8中的源码分析
见3
7.LinkedHashMap的底层实现
* //【LinkedHashMap是HashMap的子类,底层使用的结构和HashMap相同,LinkedHashMap存储时还是用HashMap里定义的数组】 * //【区别在于LinkedHashMap内部定义了一个Entry,替换了HashMap中的Node】 * //【LinkedHashMap可以按照添加的顺序遍历】 * //【把LinkedHashMap底层实现原理搞明白了,LinkedHashSet也就懂了】 * //【LinkedHashMap里的添加方法,用的是父类的,put调putVal * final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict), * 但它有自己重写的newNode * Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) { LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash, key, value, e); linkNodeLast(p); return p; }】 * 源码中: * 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); } }
* 【HashSet与HashMap * 1.new一个HashSet,本质是new一个HashMap * public HashSet() { map = new HashMap<>(); } 2.向HashSet中add元素时,相当于把元素放入Map。 放入Set中的数据相当于Map中的key, Map中的value是PRESENT,private static final Object PRESENT = new Object();一个static final常量,不是null public boolean add(E e) { return map.put(e, PRESENT)==null; } 】
8.Map中的常用方法1
添加、删除、修改操作: Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中 void putAll(Map m):将m中的所有key-value对存放到当前map中 Object remove(Object key):移除指定key的key-value对,并返回value void clear():清空当前map中的所有数据 元素查询的操作: Object get(Object key):获取指定key对应的value boolean containsKey(Object key):是否包含指定的key boolean containsValue(Object value):是否包含指定的value int size():返回map中key-value对的个数 boolean isEmpty():判断当前map是否为空 boolean equals(Object obj):判断当前map和参数对象obj是否相等
9.Map中的常用方法2
元视图操作的方法: //【Map的遍历:如何遍历Map的key、value、key和value】 //【Collection的遍历:迭代器、foreach、普通for循环。Map没有迭代器】 //【Map是由key和value构成的,所有的key是一个Set,拿到所有的key构成的Set,用Set的迭代器就行了, //拿到所有的value构成的Collection,用Collection的迭代器就行了 //键值对整体构成一个Set,拿到所有键值对构成的Set】 Set keySet():返回所有key构成的Set集合 Collection values():返回所有value构成的Collection集合 Set entrySet():返回所有key-value对构成的Set集合 //【遍历并不是为了看Map里的元素,如果仅仅是为了看,直接System.out.println(map); //用户注册,要填很多信息,name-编辑框、age-编辑框、学历-编辑框等等, //name、age、学历都可以作为key,编辑框填的内容就是value。 //填完后,把数据发送到后台,后台用Map装,Map里面都是键值对, //把Map数据装入数据库,数据库有很多字段:name、age、学历, //把Map中每一个key和value取出放入数据库,一个一个取的过程就是遍历。 //把数据在后台,把数据转化为JSON呈现到浏览器】 *总结:常用方法: * 添加:put(Object key,Object value) * 删除:remove(Object key) * 修改:put(Object key,Object value) * 查询:get(Object key) * 长度:size() * 遍历:keySet() / values() / entrySet()
public class MapTest {
/*
元视图操作的方法:
Set keySet():返回所有key构成的Set集合
Collection values():返回所有value构成的Collection集合
Set entrySet():返回所有key-value对构成的Set集合
*/
@Test
public void test5(){
Map map = new HashMap();
map.put("AA",123);
map.put(45,1234);
map.put("BB",56);
//遍历所有的key集:keySet()
Set set = map.keySet();
Iterator iterator = set.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
System.out.println();
//遍历所有的value集:values()
Collection values = map.values();
for(Object obj : values){
System.out.println(obj);
}
/*
AA
BB
45
123
56
1234
*/
System.out.println();
//遍历所有的key-value
//方式一:entrySet()
Set entrySet = map.entrySet();
Iterator iterator1 = entrySet.iterator();
while (iterator1.hasNext()){
Object obj = iterator1.next();//【obj就是一个一个的entry】
//entrySet集合中的元素都是entry
Map.Entry entry = (Map.Entry) obj;
System.out.println(entry.getKey() + "---->" + entry.getValue());
}
System.out.println();
//方式二:
Set keySet = map.keySet();
Iterator iterator2 = keySet.iterator();
while(iterator2.hasNext()){
Object key = iterator2.next();
Object value = map.get(key);
System.out.println(key + "=====" + value);
}
}
/*
元素查询的操作:
Object get(Object key):获取指定key对应的value
boolean containsKey(Object key):是否包含指定的key
boolean containsValue(Object value):是否包含指定的value
int size():返回map中key-value对的个数
boolean isEmpty():判断当前map是否为空
boolean equals(Object obj):判断当前map和参数对象obj是否相等
//【要想结果为true,obj也必须是Map,且内容一样】
*/
@Test
public void test4(){
Map map = new HashMap();
map.put("AA",123);
map.put(45,123);
map.put("BB",56);
// Object get(Object key)
System.out.println(map.get(45));
//containsKey(Object key)
boolean isExist = map.containsKey("BB");//【判断哈希值->判断equals】
System.out.println(isExist);
isExist = map.containsValue(123);//【有两个123,找到是true就不会再往下找了】
System.out.println(isExist);
map.clear();
System.out.println(map.isEmpty());
}
/*
添加、删除、修改操作:
Object put(Object key,Object value):将指定key-value添加到(或【修改】)当前map对象中
void putAll(Map m):将m中的所有key-value对存放到当前map中
Object remove(Object key):移除指定key的key-value对,并返回value
void clear():清空当前map中的所有数据
*/
@Test
public void test3(){
Map map = new HashMap();
//添加
map.put("AA",123);//【实际开发中,key的类型是统一的,value的类型是统一的。到泛型再讲。key是String的情况比较多。】
map.put(45,123);
map.put("BB",56);
//【修改/替换】
//【"AA"哈希值相同,value不同,修改】
map.put("AA",87);//【Map算哈希值,是算key的哈希值】
System.out.println(map);
Map map1 = new HashMap();
map1.put("CC",123);
map1.put("DD",123);
map.putAll(map1);
System.out.println(map);
//remove(Object key)
Object value = map.remove("CC");
System.out.println(value);
System.out.println(map);
//clear()
map.clear();//与map = null操作不同
//【是让Map里的元素都为null,而不是让Map指向null】
System.out.println(map.size());//【不会空指针异常】//0
System.out.println(map);//【{}】
}
// LinkedHashMap可以按照添加的顺序遍历,能记录添加的顺序
@Test
public void test2(){
Map map = new HashMap();
map = new LinkedHashMap();
map.put(123,"AA");
map.put(345,"BB");
map.put(12,"CC");
System.out.println(map);
}
@Test
public void test1(){
Map map = new HashMap();
// map = new Hashtable();
map.put(null,123);
//【HashMap能存null的应用场景:
// 在浏览器登录账号name,密码password,点登录,
// 数据保存在map中,怎么保存?
// 两个键值对。
// key value
// 第一个键值对: name 小明
// 第二个键值对: password 1234567,
//然后在数据库中校验
// 如果用户忘了写,那value就是null,在Hashtable中就会导致空指针异常】
}
}
10.TreeMap的两种添加方式
public class TreeMapTest {
//向TreeMap中添加key-value,要求key必须是由【同一个类】创建的对象
//因为要【按照key进行排序】:自然排序 、定制排序
//【不能按照value排序】
//自然排序
@Test
public void test1(){
TreeMap map = new TreeMap();
User u1 = new User("Tom",23);
User u2 = new User("Jerry",32);
User u3 = new User("Jack",20);
User u4 = new User("Rose",18);
map.put(u1,98);//【开发中,key、value一般都是固定类型,讲泛型再强调这件事】
map.put(u2,89);
map.put(u3,76);
map.put(u4,100);
Set entrySet = map.entrySet();
Iterator iterator1 = entrySet.iterator();
while (iterator1.hasNext()){
Object obj = iterator1.next();
Map.Entry entry = (Map.Entry) obj;
System.out.println(entry.getKey() + "---->" + entry.getValue());
}
}
//定制排序
@Test
public void test2(){
TreeMap map = new TreeMap(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
if(o1 instanceof User && o2 instanceof User){
User u1 = (User)o1;
User u2 = (User)o2;
return Integer.compare(u1.getAge(),u2.getAge());
}
throw new RuntimeException("输入的类型不匹配!");
}
});
User u1 = new User("Tom",23);
User u2 = new User("Jerry",32);
User u3 = new User("Jack",20);
User u4 = new User("Rose",18);
map.put(u1,98);
map.put(u2,89);
map.put(u3,76);
map.put(u4,100);
Set entrySet = map.entrySet();
Iterator iterator1 = entrySet.iterator();
while (iterator1.hasNext()){
Object obj = iterator1.next();
Map.Entry entry = (Map.Entry) obj;
System.out.println(entry.getKey() + "---->" + entry.getValue());
}
}
}
public class User implements Comparable{
private String name;
private int age;
public User() {
}
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public boolean equals(Object o) {
System.out.println("User equals()....");
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
if (age != user.age) return false;
return name != null ? name.equals(user.name) : user.name == null;
}
@Override
public int hashCode() { //return name.hashCode() + age;
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
return result;
}
//按照姓名从大到小排列,年龄从小到大排列
@Override
public int compareTo(Object o) {
if(o instanceof User){
User user = (User)o;
// return -this.name.compareTo(user.name);
int compare = -this.name.compareTo(user.name);
if(compare != 0){
return compare;
}else{
return Integer.compare(this.age,user.age);
}
}else{
throw new RuntimeException("输入的类型不匹配");
}
}
}
11.Properties处理属性文件
public class PropertiesTest {
//Properties:常用来处理配置文件。【key和value都是String类型】
//【配置文件:物理上存在的文件。用Properties把配置文件读到内存中
// 比如修改了idea的配置信息,怎么在idea运行中体现这些修改了?idea的指令读配置文件。
// java中,用Properties读配置文件】
//【idea配置文件,默认在工程下,右键new File(name写jdbc.properties)
// 或Resource Bundle(写jdbc,自动补充.properties),
// (后面讲java操作数据库的技术,就是jdbc)。
// jdbc.properties写name不要name = Tom,会认为name是" Tom"而不是"Tom"】
public static void main(String[] args) {
FileInputStream fis = null;
try {
Properties pros = new Properties();
fis = new FileInputStream("jdbc.properties");//【流要有关闭操作】
pros.load(fis);//加载【流】对应的文件
String name = pros.getProperty("name");
String password = pros.getProperty("password");
System.out.println("name = " + name + ", password = " + password);
} catch (IOException e) {
e.printStackTrace();
} finally {
if(fis != null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
12.Collections工具类常用方法
* xxx * xxxs:工具类;xxx不能造对象,通过xxxs调方法返回一个xxx类型的对象; * xxx Factory调方法返回一个xxx类型的对象
* Collections:操作Collection、【Map】的工具类
/* reverse(List):反转 List 中元素的顺序 shuffle(List):对 List 集合元素进行随机排序 sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序 sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序 swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换 Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素 Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回给定集合中的最大元素 Object min(Collection) Object min(Collection,Comparator) int frequency(Collection,Object):返回指定集合中指定元素的出现次数 void copy(List dest,List src):将src中的内容复制到dest中 //【 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"); if (srcSize < COPY_THRESHOLD || (src instanceof RandomAccess && dest instanceof RandomAccess)) { for (int i=0; i<srcSize; i++) dest.set(i, src.get(i)); } else { ListIterator<? super T> di=dest.listIterator(); ListIterator<? extends T> si=src.listIterator(); for (int i=0; i<srcSize; i++) { di.next(); di.set(si.next()); } } }】 boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值 */
public class CollectionsTest {
//返回的list1即为线程安全的List
List list1 = Collections.synchronizedList(list);
}
@Test
public void test1(){
List list = new ArrayList();
list.add(123);
list.add(43);
list.add(765);
list.add(765);
list.add(765);
list.add(-97);
list.add(0);
System.out.println(list);
// Collections.reverse(list);
// Collections.shuffle(list);
// Collections.sort(list);
// Collections.swap(list,1,2);
int frequency = Collections.frequency(list, 123);
System.out.println(list);
System.out.println(frequency);
}
}