1.集合与数组的关系及其概述
1.1集合与数组都是容器
集合和数组都是对多个数据进行存储操作的结构,简称Java容器。
注意:此时的存储是内存层面的存储,而非持久化的存储。
1.2数组存储的特点与缺点
数组存储的特点:1)一旦初始化之后,其长度就确定了;2)数组一旦定义好,其元素的类型也就确定了,我们也就只能操作指定类型的数据。
数组存储的缺点:1)一旦初始化之后,其长度就不可修改;2)数组中提供的方法十分有限;3)若想获取数组中实际元素的个数,没有现成的属性和方法可用;4)数组存储的是有序、可重复的数据,无法满足无序、不可重复数据的存储需求。
1.3集合框架
Java集合可分为Collection体系和Map体系
<1>Collection接口:单列数据,定义了存取一组对象的方法的集合
1)List接口:元素有序,可重复的集合,(习惯上称为“动态数组” );→常用实现类:ArrayList,LinkedArrayList,Vector。
2)Set接口:元素无序,不可重复的集合;→常用实现类:HashSet,LinkedHashSet,TreeSet。
3)Quene接口。
<2>Map接口:双列数据,保存具有映射关系的“key-value对”的集合。→常用实现类:HashMap,LinkedHashMap,TreeMap,Hashtable,Properties。
2.Collection接口常用的方法
注意:向Collection接口的实现类的对象中添加数据obj时,要求obj所在类要重写equals()。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
public class CommonlyUsedMethod
{
public static void main(String[] args)
{
Collection coll=new ArrayList();
/**
1.add(Object e):boolean 将元素e添加到集合coll中
*/
System.out.println("1.add(Object e)的使用");
coll.add(1234);
coll.add("aaa");
/**
2.size():int 获取集合中元素的个数
*/
System.out.println("2.size()的使用");
System.out.println(coll.size());//输出2
/**
3.addAll(Collection coll):boolean 将集合coll中的元素添加到当前的集合中
*/
System.out.println("3.addAll(Collection coll)的使用");
Collection coll2=new ArrayList();
coll2.addAll(coll);
coll2.add(5678);
System.out.println(coll2.size());//输出3
System.out.println(coll2);//输出集合coll2中的元素:[1234, aaa, 5678]
/**
4.isEmpty():boolean 判断集合是否为空,即集合中是否有元素存在
*/
System.out.println("4.isEmpty()的使用");
System.out.println(coll2.isEmpty()); //输出false
/**
5.clear():void 清空集合中的元素
*/
System.out.println("5.clear()的使用");
coll.clear();
coll2.clear();
/**
6.contains(Object o):boolean 判断集合中是否含有元素o
*/
System.out.println("6.contains(Obkect o)的使用");
coll2.add(123);coll2.add(456);coll2.add(true);coll2.add(new String("Tom"));
System.out.println(coll2.contains(123));//输出true
System.out.println(coll2.contains(new String("Tom"))); //输出true,这里其实又造了一个String对象,由此可见查看是否包含有元素,调的是当前类的equals()方法
/**
7.containsAll(Collection coll):boolean 判断形参集合coll中的元素是否都在当前集合中
*/
System.out.println("7.containsAll(Collection coll)的使用");
coll.add(123);coll.add(true);
System.out.println(coll2.containsAll(coll)); //输出true
/**
8.remove(Object o):boolean 删除当前集合中与元素o值相等的元素,删除成功返回true(这里也是调了equals()方法)
*/
System.out.println("8.remove(Object o)的使用");
System.out.println(coll2); //输出[123, 456, true, Tom]
System.out.println(coll2.remove(456)); //输出true
System.out.println(coll2); //输出[123, true, Tom]
/**
9.removeAll(Collection coll):boolean 差集:当前集合\coll, 在当前集合中删除集合coll中所包含的元素,只要有一个当前集合中有一个元素被删除就返回true
*/
System.out.println("9.removeAll(Collection coll)的使用");
System.out.println(coll); //输出[123, true]
coll2.removeAll(coll);
System.out.println(coll2);//输出Tom
/**
10.retainAll(Collection coll):boolean 交集,获取当前当前集合和集合coll的交集,并把交集赋给当前集合,只要存在交集就返回true
*/
System.out.println("10.retainAll(Collection coll)的使用");
coll.add(456);coll.add(789);
System.out.println(coll);//coll为:[123, true, 456, 789]
coll2.add(456);coll2.add(789);
System.out.println(coll2);//coll2为:[Tom, 456, 789]
System.out.println(coll2.retainAll(coll));//输出true
System.out.println(coll2);//此时,coll2为:[456, 789]
/**
11.equals(Object o):boolean 比较当前集合和形参集合o是否相等(这里List注重有序;而Set注重无序)
*/
System.out.println("11.equals(Object o)的使用");
Collection coll3=new ArrayList();
coll3.add(456);coll3.add(789);
System.out.println(coll2.equals(coll3));//输出true
Collection coll4=new ArrayList();
coll4.add(789);coll4.add(456);
System.out.println(coll2.equals(coll4));//输出false,因为这里ArrayList需要有序
/**
12.hashCode():int 返回当前集合的哈希值
*/
System.out.println("12.hashcode()的使用");
System.out.println(coll2.hashCode());//输出15886
/**
13.toArray():Object[] 将当前集合中的元素存储在一个数组中
*/
System.out.println("13.toArray()的使用");
Object[] arr=coll2.toArray();
for (Object o: arr)
{
System.out.println(o);//遍历输出数组arr中的(其实也是coll2中的)的元素
}
/**
14.拓展补充:Arrays类中的asList方法,可将数组转化为集合
*/
System.out.println("14.Array中的asList方法");
String[] strs=new String[]{"qwer","tyui","asdf"};
List<String> strings = Arrays.asList(strs);
System.out.println(strings);
/**
15.Iterator():Iterator<E> 返回Iterator接口的实例,用于遍历集合元素
*/
}
}
3.集合元素的遍历
3.1使用迭代器Iterator接口遍历集合
<1>迭代器模式
访问容器对象中的各个元素,而又不暴露该对象的内部细节,主要用于遍历Collection中的元素。
iterator()仅用于遍历集合,且集合对象每次调用iterator(),都会得到一个全新的迭代器对象,默认游标在集合的第一个元素之前。
<2>迭代器的hasNext()和next()方法
hasNext():boolean→以当前位置为基础,判断下一位置是否还有元素
next():E→指针下移,并将下移以后集合位置上的元素返回。
代码:
对于代码中的“Iterator iterator=coll.iterator();”这一句,是因为集合中的iterator()方法返回的是Iterator接口的对象,其实际过程为:ArrayList实现类中定义了一个内部类Itr,Itr实现了Iterator接口,即“private class Itr implemens Iterator<E>”,重写了其中的next(),hasNext()等方法,这里的iterator()方法的方法体中只有一句,即:“return new Itr();”,所以当调用iterator()方法时,得到的是Iterator接口实现类的对象。
@Test
public void nextAndhasNextTest()
{
Collection coll = new ArrayList();
coll.add(123);
coll.add(new Person("Jerry", 20));
coll.add(new String("Tom"));
coll.add(false);
Iterator iterator = coll.iterator();
while (iterator.hasNext())
{
System.out.println(iterator.next());
}
}
<3>一种用迭代器遍历集合的错误示范
//下面这样写会出现错误,陷入死循环,不断输出集合的第一个元素123
//原因是因为:集合对象每次调用iterator()方法都得到一个全新的迭代器对象!!
// while(coll.iterator().hasNext())
// {
// System.out.println(coll.iterator().next());
// }
<4>迭代器的remove()方法(注意这是迭代器哦的remove方法)
remove():void →删除当前迭代器所在位置的元素
注意:如果还未调用next()或在上一次调用next()方法后已经调用了remove()方法,再调用remove()都会报IIlegalStateException异常。
@Test
public void removeTest()
{
Collection coll = new ArrayList();
coll.add(123);
coll.add(new Person("Jerry", 20));
coll.add(new String("Tom"));
coll.add(false);
Iterator iterator1 = coll.iterator();
while (iterator1.hasNext())
{
Object o = iterator1.next();
if ("Tom".equals(o))
{
iterator1.remove();
}
}
iterator1=coll.iterator();//因为上面的while循环使得iterator1指向了最后的位置,所以这里重置一下
while (iterator1.hasNext())
{
System.out.println(iterator1.next());
}
}
3.2使用增强for循环遍历集合
<1>快捷键:集合名.for 或者 iter
<2>格式: for(集合中元素的类型 局部变量名:集合对象名) {......}
<3>其内部实质上仍调用了迭代器
<4>增强for循环不会改变原有集合中元素的值
4.Collection子接口之——List接口
4.1List接口概述
<1>List集合类中元素有序,且可重复,每个元素都有其对应的顺序索引;
<2>List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素;
<3>List接口的三个常用类:ArrayList,LinkedList,Vector的异同
同:都实现了List接口,存储数据的特点相同:即有序,可重复的数据
异:1)ArrayList:线程不安全,效率高,底层使用Object[] elementData存储;2)LinkedList:对于频繁地插入、删除操作,使用LinkedList比使用ArrayList的效率要高,底层使用双向链表存储;3)Vector:是List接口的古老实现类,线程安全,效率低,底层使用Object[] elemenData存储,且Vector是同步类,属于强同步类,故开销大于ArrayList。
4.2ArrayList源码分析
ArrayList底层使用了数组Object[] elementData
<1>▲▲jdk7情况下:
1)就ArrayList的初始化而言,ArrayList list=new ArrayList() //底层创建了长度是10的Object[]数组,数组名为elementData
具体情况为:ArrayList.java中存在代码:
private transient Object[] elementData;
public ArrayList (int initialCapacity)
{
super();
if(initialCapacity<0)
throw new IllegalArgumentException("Illegal Capacity:"+initialCapacity);
this.elementData=new Object[initialCapacity];
}
public ArrayList()
{this(10);}//当使用空参构造器造ArrayList对象时,定义了一个容量为10的Object型数组
2)就ArrayList添加元素而言
list.add(1); //相当于elementData[0]=new Integer(1);
.......................
list.add(11);// 若此次的添加导致底层的elementData数组容量不够,则需扩容,默认情况下扩容为原来的1.5倍。同时,需将原有数组中的数据复制到新的数组中。
结论:建议开发中使用带参的构造器
<2>▲▲jdk8情况下
ArrayList list=new ArrayList(); //底层Object[] elementData初始化为{},并没有创建长度为10的数组
list.add(1);// 当第一次调用add()方法时,底层才创建了长度为10的数组,并将数据1添加到elementData数组的第一个位置上,后续的添加与扩容与jdk7无异。
<3>jdk7中ArrayList对象的创建类似于单例模式的饿汉式,而jdk8中ArrayList对象的创建类似于单例模式的懒汉式,这延迟了数组的创建,节省了内存。
4.3LinkedList源码分析
LinkedList list=new LinkedList(); //内部声明了Node类型的first和last属性,默认值为null
list.add(123); //将123封装到Node中,创建了Node对象
其中,Node定义为:
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.4Vector源码分析
在jdk7和jdk8中通过Vector()构造器创建对象时,底层都创建了长度为10的数组;在扩容方面,默认扩容为原来数组长度的两倍。
4.5List中的常用方法
这里主要讲关于索引的方法,那些继承于Collection接口的方法不在赘述。
1)void add(int index, Object element):在index位置处插入element元素
2)boolean addAll(int index, Collection coll):在index位置开始将集合coll中的所有元素添加到当前集合
3)int indexOf(Object obj):返回obj在当前集合中首次出现的位置(若不存在该元素,返回-1)
4)int lastIndexOf(Object obj):返回obj在当前集合中最后一次出现的位置
5)Object remove(index):移除index位置的元素,并返回该元素
6)Object set(int index, Object element):设置指定index位置的元素为element
7)List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的集合→[fromIndex,toIndex)
一个简单例题:
这里的remove()方法运用的是remove(int index),因为remove(Object element)还需要进行自动装箱。
4.6List中应熟记的方法
增:add(Object obj)
删:remove(int index) / remove(Object obj)
改:set(int index, Object element)
查:get(int index)
插:add(int index, Object elemrent)
长度:size()
遍历:1)Iterator迭代器方式;2)增强for循环;3)普通for循环
5.Collection子接口之——Set接口
5.1Set接口概述
<1>用来存储无序,不可重复的数据
<2>Set中没有定义新的方法,都继承于Collection
<3>Set的三个常用实现类:HashSet,LinkedHashSet,TreeSet
HashSet:是Set接口的主要实现类,线程不安全,可以存储null值;LinkedHashSet:是HashSet的子类,遍历其内部的数据时,可以按照元素添加的顺序遍历(在添加数据的同时,每个数据还维护了两个引用,记录此数据前一个数据和后一个数据)<3>TreeSet:可以照添加对象的指定属性,进行排序。
5.2理解Set存储数据的的无序性和不可重复性
以HashSet为例进行说明
<1>无序性:不等于随机性,存储的数据在底层数组中并非按照索引的顺序从前往后来添加数据,而是根据所添加元素的哈希值决定的。
<2>不可重复性:保证添加的元素与之前已有的元素按照equals()判断时,不能返回true,即相同的元素只能添加一个。
5.3Set中元素添加的过程
以HashSet为例进行说明
step1:对要添加的某元素a,先计算a的哈希值(说明在添加对象的类中需要重写hashCode()方法),将得到的哈希值再通过某种算法计算出该元素a在HashSet底层数组中的存放位置,如果这个位置上没有其他元素,则元素a添加成功;
step2:若这个位置上已经有了其他元素,则需逐个比较a和这些元素的哈希值(因为一个位置上可能存放了多个数据,这些数据通过链表相连),若a与这些元素的哈希值都不相等,则元素a添加成功;
step3:若比较过程中发现了与a哈希值相等的元素,则需调用equals()方法(说明在添加对象的类中需要重写equals()方法),若equals()返回flase,则元素a添加成功,若返回true,则添加失败。
注意:若的确进行到了step2和step3,且成功添加了该元素,元素a与之前已经存储于指定索引位置的数据以链表的方式存储。在jdk7中,元素a指向此链表中之前的元素;在jdk8中,链表中的元素指向a。
5.4hashCode()和equals()方法的重写
向Set(主要是实现类HashSet和LinkedHashSet)中添加元素时,这些元素所在的类中需要重写hashCode()和equals()。
<1>重写的基本原则:
1)同一个对象调用hashCode()方法应该返回相同的值;
2)对象中用作equals()方法比较的属性,在计算hashCode值时,也需被用到
<2>为什么使用快捷键自动生成hashCode()方法时,经常会出现31这个数字?
1)减少冲突。选择系数时要选择尽量大的系数。如果计算出来的hash地址越大,所谓的冲突就会越少;并且31是一个素数,不管用什么数来乘它,其公因数只有1和它本身;
2)31只占用了5bits,相乘造成数据溢出的概率较小;
3)提高算法效率。31可以有i*31==(i<<5)-1来表示,现在很多虚拟机里面都有做相关优化。
5.5LinkedHashSet实现类
作为HashSet的子类,在添加数据的同时,每个数据还维护了两个引用,以用来记录此数据的前一个数据和后一个数据。并且输出时是按照添加数据的顺序输出的。
与HashSet的对比:LinkedHashSet插入性能略低于HashSet(why?是不是其实底层和HashSet一样也还是数组,反而多了两个引用,这样插入时不但要移动其他元素的位置,还要把改一下前后的元素指向);对于频繁地遍历操作,LinkedHashSet效率高于HashSet。
5.6TreeSet实现类
<1>底层是红黑树,故其中的数据有序,查询速度快;
<2>向TreeSet中添加的数据,要求是相同类的对象,否则报ClassCastException错误;
<3>TreeSet中元素若要输出,必须在元素所对应的类中告知元素的排序方法,从而按照一定的顺序输出,不然就无法输出TreeSet中的元素;
<4>TreeSet中对象数据的两种排序方式
▲▲▲自然排序
TreeSet调用集合元素的compareTo(Object obj)方法来比较元素之间的大小关系,然后将集合元素按升序(默认为升序)排列。
几个要求:(以Person类对象为例)
1)若想添加一个对象(Person对象)到TreeSet时,则该对象的类必须实现Comparable接口;
2)实现Comparable的类(Person类)必须实现compareTo(Object obj)方法,两个对象即通过compareTo(Object obj)方法的返回值来比较大小,若两个对象相等,会返回0。特别注意,这里没有用equals()方法;
3)代码展示:
public class Person implements Comparable
{
public String name;
public int age;
public Person()
{}
public Person(String name,int age)
{
this.name=name;
this.age=age;
}
@Override
public int compareTo(Object o)
{
if(o instanceof Person)
{
Person person = (Person) o;
int compare = this.name.compareTo(person.name);//这里调用了String类中重写的compareTo()方法
if(compare!=0)
return compare;
else
return Integer.compare(this.age,person.age);//这里调用了包装类Integer中重写的compareTo()方法
}
else
throw new RuntimeException("类型不匹配");
}
}
▲▲▲定制排序
若集合元素所属的类没有实现Comparable接口,或者希望按照其他属性大小进行排序,则考虑使用定制排序,定义排序通过Comparator接口来实现,需要重写compare(T o1,T o2)方法。
注意:顺序相等,则返回0,变量o1的顺序在变量o2之前则返回负值数,变量o1的顺序在变量o2之后则返回正值数。以此比较!也就是说,o1-o2也就代表着升序排序;同理o2-o1代表着降序排序。如果有一个n✖2的二维数组array,则对其排序需要进行如下操作,见代码如下:
解释:对array中每个二维分量,按照第一个元素进行升序排序,如果第一个元素相等,则按照第二个元素的降序排序!!
Arrays.sort(array, new Comparator<int[]>() {
@Override
public int compare(int[] a, int[] b) {
return a[0]==b[0] ? b[1]-a[1] : a[0]>b[0];
}
});
一些要求:
1)利用compare(T o1,T o2)方法比较o1和o2的大小,若返回正整数,表示o1大于o2;若返回负整数,则反之;若返回0,表示两者相等;
2)要实现定制排序,需要将实现的Comparato接口的实例作为形参传递给TreeSet的构造器,所以只可能在main方法(或单例测试方法)内部来创建一个匿名实现类;
3)代码展示:
这个代码我不知道为什么compare方法中有throw异常,但是方法声明处不写throws来处理,也没有try-catch-finall结构,这是为什么?
public class TreeSetTest
{
@Test
public void test()
{
Comparator com = new Comparator()
{
//照年龄从小到大排列
@Override
public int compare(Object o1, Object o2)
{
if (o1 instanceof Person && o2 instanceof Person)
{
Person u1 = (Person) o1;
Person u2 = (Person) o2;
return Integer.compare(u1.age, u2.age);
} else
{
throw new RuntimeException("输入的数据类型不匹配");
}
}
};
TreeSet set = new TreeSet(com);//将Comparator接口的实例作为TreeSet构造器的形参
set.add(new Person("tom", 12));
set.add(new Person("tom", 48));
set.add(new Person("herry", 34));
set.add(new Person("mary", 26));
System.out.println(set);
}
}
5.7一个小结
集合Collection中存储的如果是自定义类的对象,需要自定义类重写的方法:
List:equals()方法;、
Set:-----对于HashSet,LinkedHashSet:equals()和hashCode()方法;
-----对于TreeSet:Comparable接口:重写compareTo()方法或Comparator接口:重写compare(Object o1, Object o2)方法
5.8例题
<1>例题一
对于一个类Person,定义两个属性String name和int age,一个带参构造器,并重写了toString(),hashCode(),equals()方法,现有如下代码,判断其输出内容。
过程解析以及输出结果见代码注释。
@Test
public void outPredict()
{
HashSet set = new HashSet();
Person p1 = new Person( "AA",1001);
Person p2 = new Person( "BB",1002);
set.add(p1);
set.add(p2);
p1.name = "CC";
/**
这里删除的仍是p1,但remove()是要先检索Set中是否有元素的哈希值与待删除元素的哈希值相等,
即,需计算此时p1对应的哈希值,即为Person("CC",1001)的哈希值,在根据该值找到底层数组上对应的位置,
发现根本没有相同的元素,所以此时返回了false,即没有删除p1
*/
set.remove(p1);
/**
如果真想删除这个Person对象Person("CC",1001),可以使用迭代器中的remove()方法
注意:迭代器的使用相当于是HashSet类对象将自己的元素暂时托管了给迭代器。所以如果要进行增删操作,
必须也要通过迭代器对象来完成。查阅源码可知,如果在迭代器里直接对HashSet类对象进行操作,
会引起元素的修改次数和期待修改次数不等进而报错。
while (iterator.hasNext())
{
Object a=iterator.next();
if(a instanceof Person)
{
if (a.equals(new Person("CC",1001)))
iterator.remove();
}
}
*/
System.out.println(set);
/**
此时也是计算Person("CC",1001)的哈希值,在找到对应的位置,不会出现在刚才定义的那两个元素的位置上,
所以这个元素可以插入
*/
set.add(new Person("CC",1001));
System.out.println(set);
/**
计算Perosn("AA",1001)哈希值并找到对应位置后,上面已经有了之前的元素Person("CC",1001),再比equals()发现不同,故可以插入
*/
set.add(new Person("AA",1001));
System.out.println(set);
}
//输出结果为:
//[Person{name='BB', age=1002}, Person{name='CC', age=1001}]
//[Person{name='BB', age=1002}, Person{name='CC', age=1001}, Person{name='CC', age=1001}]
//[Person{name='BB', age=1002}, Person{name='CC', age=1001}, Person{name='CC', age=1001}, Person{name='AA', age=1001}]
<2>例题二
代码如下,问题1)我没有注释,问题2)见大幅注释部分,一些相关说明也见注释:
MyDate类:
package exer1p546;
public class MyDate //implements Comparable
{
private int year;
private int month;
private int day;
/**
构造器
*/
public MyDate(int year,int month,int day)
{
this.year=year;
this.month=month;
this.day=day;
}
/**
输出年月日
*/
public String toDateString()
{
return year+"年 "+month+"月 "+day+"日 ";
}
public int getMonth()
{
return month;
}
public int getYear() {return year;}
public int getDay() {return day;}
// @Override
// public int compareTo(Object o)
// {
// if(o instanceof MyDate)
// {
// int a=this.year-((MyDate) o).getYear();
// if(a!=0)
// return a;
// int b=this.month-((MyDate) o).getMonth();
// if(b!=0)
// return b;
// int c=this.day-((MyDate) o).getDay();
// if(c!=0)
// return c;
// else
// return 0;
// }
// else
// throw new RuntimeException("输入有误");
// }
}
Employee类:
package exer1p546;
public class Employee implements Comparable
{
private String name;
private int age;
private MyDate birthday;
public Employee()
{}
/**
构造器
*/
public Employee(String name,int number,MyDate birthday)
{
this.name=name;
this.age=number;
this.birthday=birthday;
}
/**
重写toString方法
*/
@Override
public String toString()
{
return "Employee{" +
"name='" + name + '\'' +
", number=" + age +
", birthday=" + birthday.toDateString() +
'}';
}
public MyDate getBirthday()
{
return birthday;
}
public String getName()
{
return name;
}
public int getAge()
{
return age;
}
/**
实现了Comparable接口的compareTo方法,从而按name排序
*/
@Override
public int compareTo(Object o)
{
if(o instanceof Employee)
{
return this.name.compareTo(((Employee) o).getName());
}
else
throw new RuntimeException("类型错误!");
}
}
EmployeeTest类:
package exer1p546;
import com.sun.source.util.Trees;
import java.util.Comparator;
import java.util.TreeSet;
public class EmployeeTest
{
public static void main(String[] args)
{
/*
在EmployeeTest类中实例化Comparator接口来按生日日期排序
这里我是在MyDate类中实现了Comparable接口的compareTo方法来做的,所以代码是如下这样
其实也可以直接将MyDate中重写compareTo的代码放到这里,只需做些许改变即可
*/
// Comparator comparator=new Comparator()
// {
// @Override
// public int compare(Object o1, Object o2)
// {
// if(o1 instanceof Employee && o2 instanceof Employee)
// {
// Employee e1=(Employee)o1;
// Employee e2=(Employee)o2;
// return e1.getBirthday().compareTo(e2.getBirthday());
// }
// else
// return 0;
// }
// };
Employee e1=new Employee("liudehua",55,new MyDate(1968,12,3));
Employee e2=new Employee("liming",54,new MyDate(1978,9,3));
Employee e3=new Employee("zhangxueyou",56,new MyDate(1967,1,3));
Employee e4=new Employee("guofucheng",50,new MyDate(1971,11,4));
// TreeSet set1=new TreeSet(comparator);
TreeSet set1=new TreeSet();
set1.add(e1);
set1.add(e2);
set1.add(e3);
set1.add(e4);
System.out.println(set1);
}
}
6.Map接口
6.1Map接口概述
<1>Map:存储双列数据(key—value对);
<2>Map接口的常用实现类及其简单对比
1)HashMap:是Map的最主要实现类,底层为数组+链表+红黑树,线程不安全,效率高,可以存储null的key和value
-------LinkedHashMap:是HashMap的子类,保证在遍历Map中的元素时,可以按照添加的 顺序实现遍历(原因是因为在原有的HashMap底层结构的基础上, 在每个元素上添加了一对指针,分别指向添加的前一个元素和后一 个元素)
2)TreeMap:保证按照添加的key-value对进行排序,实现排序遍历,底层为红黑树。此时需要考 虑key的自然排序或者定制排序。
3)Hashtable:古老的实现类,线程安全,效率低,存储null的key和value
------Properties:常用来处理配置文件,其key和value都是String类型的
🔺🔺HashMap,HashSet,Hashtable的区别与联系:三者的区别
6.2Map结构的理解
一个key-value键值对构成了一个Entry(或Node)对象
<1>Map中的Entry:无序的、不可重复的,使用Set储存所有的Entry;
<2>Map中的key:无序的、不可重复的(故对于储存key的有关实现类而言,对HashMap,需要重写hashCode()和equals()方法;对TreeMap,需要重写自然排序算法或定制排序算法),使用Set储存所有的key;
<3>Map中的value:无序的,可重复的,使用Collection储存所有的value(value所在类需要重写equals()方法)。
6.3Map中常用的方法
额外添加的:
<1>V getOrDefault (Object key, V defaultValue) :意思就是当Map集合中有这个key时,就返回这个key对应的value值,如果没有这个key就使返回默认值defaultValue;
<2>V merge(Object key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) : 其中BiFunction接口接受两个参数, 返回一个参数,merge方法可以这么理解。如上式,1)key不存在等于是map.put(key, value),后面的处理失效,2)key存在,value就会变成你处理后的value值。
eg:当前存在一个名为map的HashMap型对象,里面存有数据(1,1),(2,3),运行代码
map.merge(1, 2, (v1,v2)->v1+v2);// map中的数据变为(1,3), (2,3)
map.merge(3,1,(v1+v2)->v1+v2); // map中的数据变为(1,3),(2,3),(3,1)
<3>replace(Object key, V newvalue ):将指定键key所对应的value值改为新的的value值;
6.4HashMap的底层实现原理
<1>在jdk7中的实现原理
1)提前说明几个量:
▲DEFAULT_INITIAL_CAPACITY:刚开始创建的数组的长度;
▲threshold:数组扩容的阈值(=(int)Math.min(capacity*loadFactor, MAXIMUM_CAPACITY+1)),loadFactor为负载因子;
▲hash(key)方法:在使用hashCode()计算一次key的哈希值之后,在使用hash()方法计算一次,以获取更好的散列值;
▲indexFor(int, int)方法:计算元素在数组中的位置;
对其put()方法(即添加元素的方法,这里用的不是add()),有如下源码:
public V put(K key,V value)
{
if(key==null) //若key为null
return putForNullKey(value);
int hash=hash(key);
int i=indexFor(hash,table.length); //hash:key经过hashCode和hash方法计算后得到的值;table:数组名
for(Entry<K,V>e=table[i];e!=null;e=e.next)
{
Object k;
if(e.hash==hash &&(k=e.key)==key||key.equals(k))
//如果两个键值对哈希值相等并且equals()返回true,则将新值代替旧值
{
V oldValue=e.value;
e.value=value;
e.recordAccess(this);
return oldValue;
}
}
//运行到这里的话,说明这个元素被成功插入了
modCount++;
addEntry(hash,key,value,i);
return null;
}
void addEntry(int hasn,K key,V value,int bucketIndex)
{
if(size>= threshold && (null!=table[bucketIndex]))
//这句的意思是如果数组长度大于threshol
//且这个元素准备插入的位置上已经有元素了
//则扩容为原来数组长度的两倍
//这是为了让每个位置上的链表尽可能的少一些
{
resize(2*table.length);
hash=(null!=key) ? hash(key) : 0;
bucketIndex=indexFor(hash,table.length); //数组扩容后,重新计算该元素应该插入的位置
}
createEntry(hash,key,value,buckrtIndex);
}
2)文字叙述过程
HashMap map = new HashMap():
在实例化以后,底层创建了长度是16的一维数组Entry[] table。
...可能已经执行过多次put...
map.put(key1,value1):
step1:调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置,如果此位置上的数据为空,此时的key1-value1添加成功。
step2:如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。
step3:如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用key1所在类的equals(key2)方法,比较:如果equals()返回false:此时key1-value1添加成功,如果equals()返回true:使用value1替换value2。
补充:
①关于step2和step3:此时key1-value1和原来的数据以链表的方式存储;
②在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的2倍,并将原的数据复制过来;
<2>HashMap在jdk8与jdk7相比在底层实现方面的不同
1)jdk8中,对于HashMap map=new HashMap(),底层没有创建长度为16的数组;
2)jdk8中,数组类型为Node[],而不是Entry[];
3)首次调用put()方法时,才创建长度为16的数组;
4)形成链表结构时,jdk7中,在头部添加,即新的元素指向旧的元素;jdk8中,在尾部添加,即旧的元素指向新的元素;
5)jdk7中底层结构只有:数组+链表;jdk8中底层结构有:数组+链表+红黑树;
6)jdk8中,当数组的某一个索引位置上的元素以链表形式存在的数据个数>=7且当前数组的长度>64时,此时此索引位置上的数据由链表改为红黑树(提高了查询效率)储存;小于64子需要扩容。
<3>一个问题:负载因子的大小,对HashMap有什么影响?
6.3LinkedHashMap的底层实现原理
LinkedHashMap底层使用的结构与HashMap相同,因为LinkedHashMap继承于HashMap,区别在于:LInkedHashMap内部提供了Entry内部类,替换了HashMap中的Node。
可以看到,这里多了before和after两个属性,其分别指向前后两个元素。
6.5TreeMap的使用
底层使用红黑树,TreeMap按照key进行排序,并且key必须时同一个类创建的对象,否则抛出异常。
对key的两种排序措施:
1)自然排序
TreeMap的key所在的类必须实现Comparable接口,重写x.compareTo(y)方法;
2)定制排序
当创建TreeMap对象时,在形参处传入一个Comparator对象,该对象负责对TreeMap中的所有key进行排序,在这个Comparator对象中使用匿名实现类的方法重写compare(x1,x2)方法。
6.6Properties的使用
Properties类是Hashtable的子类,该对象用于处理属性文件。由于属性文件里的key、value都是字符串类型,所以Properties中存储的key和value也都是String类型的数据。
Properties对象存取数据时,建议使用setProperty(String key, String value)方法和getProperty(String key)方法。
7.Collections工具类的使用
是一个操作Collection和Map对象的工具类
7.1常用的方法
方法 | 作用 |
reverse(List) | 反转List中元素的顺序 |
shuffle(List) | 对List集合元素进行随机排序 |
sort(List) | 根据元素的自然排序对指定List集合元素升序排序 |
sort(List, Comparator) | 根据指定的Comparator产生的顺序对List集合元素进行排序 |
swap(List,int,int) | 将指定 list 集合中的 i 处元素和 j 处元素进行交换 |
Object max/min(Collection) | 根据元素的自然顺序,返回给定集合中的最大/最小元素 |
Object max/min(Collection,Comparator) | 根据 Comparator 指定的顺序,返回给定集合中的最大/最小元素 |
int frequency(Collection,Object) | 返回指定集合中指定元素的出现次数 |
void copy(List dest,List src) | 将src中的内容复制到dest中 |
boolean replaceAll(List list,Object oldVal,Object newVal) | 使用新值替换 List 对象的所旧值 |
注意,对于其中的void copy(List dest,List src)方法,不能写成如下形式:
@Test
public void copyMethod()
{
List src=new ArrayList();
src.add(123);
src.add(7);
List dest=new ArrayList();
//下面两行错误
Collections.copy(dest.src);
System.out.println(dest);
}
因为新建一个ArrayList对象时,其中没有定义相关容器来存储数据,运行会报IndexOutOfBoundException异常,可以改写如下:
@Test
public void copyMethod()
{
List src=new ArrayList();
src.add(123);
src.add(7);
List dest=Arrays.asList(new Object[src.size()]);
Collections.copy(dest,src);
}
7.2同步控制
Collections类中提供了多个synchronizedXxx()方法,此方法可以将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题。
eg:synchronizedMap(Map map);
List dest=new ArrayList(); List dest1=Collections.synchronizedList(dest);