Java基础复习-集合
本文仅对java学习知识的查缺补漏
集合框架概述
- 集合、数组都是对多个数据进行存储操作的结构,简称Java容器。(这里讲的存储都指的是对内存的操作)
- 数组在存储多个数据方面的特点:
- 一旦初始化以后,长度就确定了,长度不可修改;
- 数组一旦定义好后,其元素的类型也就确定了。比如:
String[] arr; int[] arr1;
; - 数组中提供的方法非常有限,对于添加、删除、插入数据等操作,非常不便。
- 数组存储数据的特点:有序、可重复;对于无序、不可重复的需求,不能满足。
Java集合可以分为Collection
和Map
两个体系
Collection接口
:单列数据,定义了存取一组对象的方法的集合List
:元素有序,可重复的集合(动态数组)ArrayList
、LinkedList
、Vector
Set
:元素无序,不可重复的集合(集合)HashSet
、LinkedHashSet
、TreeSet
Map接口
:双列数据,保存具有映射关系key-value
对的集合HashMap
、LinkedHashMap
、TreeMap
、Hashtable
、Properties
Collection常用方法
向Collection
接口的实现类的对象中添加数据obj时,要求obj所在类重写equals()
例子1
@Test
public void test1(){
Collection collection = new ArrayList();
//add(Object o):将元素o添加到集合collection中
collection.add("AA");
collection.add("BB");
collection.add(1234); //自动装箱
collection.add(new Date());
//size():获取添加元素的个数
System.out.println(collection.size());
//addAll(Collection o):将集合o中的所有元素添加到当前集合中
Collection collection1 = new ArrayList();
collection1.add("BCD");
collection1.add(456);
collection.addAll(collection1);
System.out.println(collection.size());
System.out.println(collection);
//clear():清空集合元素
collection.clear();
//isEmpty():判断当前集合是否为空
System.out.println(collection.isEmpty());
}
例子2
@Test
public void test1(){
Collection collection = new ArrayList();
collection.add(123);
collection.add(456);
collection.add(new String("Mike"));
collection.add(true);
Person person = new Person("老麦", 100);
collection.add(person);
collection.add(new Person("老麦", 100));
//1.contains(Object obj):判断当前集合是否包含obj
//在判断时会调用obj对象所在类的equals()方法
System.out.println(collection.contains(123)); //true
System.out.println(collection.contains(new String("Mike"))); //true,这里调用的是String的equals方法
System.out.println(collection.contains(person)); //true
System.out.println(collection.contains(new Person("老麦", 100))); //false,因为Person类没有重写equals方法,所以调用的是父类Object的equals方法
//2.containsAll(Collection coll):判断形参coll中所有元素是否都存在于当前集合中
Collection collection1 = Arrays.asList(456,123);
System.out.println(collection.containsAll(collection1));
}
例子3
@Test
public void test2(){
Collection collection = new ArrayList();
collection.add(123);
collection.add(456);
collection.add(new String("Mike"));
collection.add(true);
collection.add(new Person("老麦", 100));
collection.add(123);
//remove(Object obj):只移除第一个相同的元素,后面的相同不会移除
// collection.remove(123);
System.out.println(collection);
//removeAll(Collection coll):差集,从当前集合中移除coll中所有元素,所有有相同的元素都会移除
Collection collection1 = Arrays.asList(123,4567);
collection.removeAll(collection1);
System.out.println(collection);
//retainAll(Collection coll):交集,获取当前集合和coll集合的交集,并返回给当前集合
Collection collection2 = Arrays.asList(123,456,789);
collection.retainAll(collection2);
System.out.println(collection);
//equals(Collection coll):判断集合中所有元素是否相同,包括元素的顺序,这里省略代码
}
@Test
public void test3(){
Collection collection = new ArrayList();
collection.add(123);
collection.add(456);
collection.add(new String("Mike"));
collection.add(true);
collection.add(new Person("老麦", 100));
collection.add(123);
//hashCode():返回当前对象的哈希值
System.out.println(collection.hashCode());
//集合---->数组:toArray()
Object[] objects = collection.toArray();
for (int i = 0; i < objects.length; i++) {
System.out.println(objects[i]);
}
System.out.println("*************************");
//数组---->集合:asList()
List<String> list = Arrays.asList(new String[]{"abc", "def", "mno"});
System.out.println(list);
//细节:如果用基本数据类型当数组形参的话,asList方法会将所有元素当成一个数,不是多个数
List<int[]> ints = Arrays.asList(new int[]{123, 456});
System.out.println(ints);
System.out.println(ints.size());
//改正:用包装类或者不new,直接填值
List<Integer> list1 = Arrays.asList(new Integer[]{123, 456});
System.out.println(list1);
System.out.println(list1.size());
}
Iterator:迭代器
Iterator
:仅用于遍历集合,Iterator
本身并不提供承载对象的能力。如果需要创建Iterator
对象,则必须有一个被迭代的集合。- 集合对象每次调用
iterator()
方法都会得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前。 next()
:下一个元素(执行的时候,指针先下移一位,再拿到元素)。hasNext()
:有没有下一个元素。remove()
:移除某个元素,没有调用next()
之前不可以调用移除的方法,因为指针还指在要移除元素的前一个。- 迭代器操作的是原来的集合,不是副本。
- 注意:迭代器用完一次后,要重新建一个新的,相当于指针遍历到后面了,需要重新把指针指到第一个元素的前一位。
/**
* 集合元素遍历,使用迭代器Iterator接口
* 内部方法:hasNext() 和 next()
* @Author: fxx
* @Date: 2020/12/25 22:13
*/
@Test
public void test(){
Collection collection = new ArrayList();
collection.add(123);
collection.add(456);
collection.add(new String("Mike"));
collection.add(true);
collection.add(new Person("老麦", 100));
collection.add(123);
Iterator iterator = collection.iterator();
//迭代器正确用法
while(iterator.hasNext()){
System.out.println(iterator.next());
}
//下面是错误用法
//错误用法1:
while((iterator.next()) != null){
System.out.println(iterator.next());
}
//错误用法2:
while(collection.iterator().hasNext()){
System.out.println(collection.iterator().next());
}
}
foreach
@Test
public void test(){
Collection collection = new ArrayList();
collection.add(123);
collection.add(456);
collection.add(new String("Mike"));
collection.add(true);
collection.add(new Person("老麦", 100));
collection.add(123);
// Iterator iterator = collection.iterator();
//for(集合元素的类型 局部变量 :集合对象)
//foreach循环,内部调用的还是迭代器,每次取到对应元素的值,赋给局部变量,
//所以foreach循环不能用来修改值
for (Object o :
collection) {
System.out.println(o);
}
}
List
ArrayList
:作为List
接口的主要实现类;线程不安全,效率高;底层使用Object[]
存储(数组顺序存储)LinkedList
:对于频繁的插入、删除操作,使用此类效率比ArrayList
高;底层使用双向链表存储;Vector
:作为List
接口的古老实现类;线程安全,效率低;底层使用Object[]
存储(数组存储)
说明+常用方法
/**
* List接口的类
* ArrayList源码分析:
*
* jdk7下:
* ArrayList list = new ArrayList();底层创建了长度为10的Object[]数组elementData
* list.add(123); //elementData[0] = new Integer(123);
* 如果添加的时候发现elementData数组容量不够,则扩容
* 默认情况下,扩容为原来容量的1.5倍
* (原来的容量右移一位+原来容量:int newCapacity = oldCapacity + (oldCapacity >> 1);),
* 同时需要将原有数组中的数据复制到新数组中。
*
* 结论:建议开发中使用带参的构造器:ArrayList(int capacity)知道容量的情况下
*
* jdk8下:
* ArrayList list = new ArrayList();底层Object[] elementData初始化为{},并没有创建长度为10的数组
* list.add(123); //第一次调用add()时,底层才创建了长度为10的数组,并将数据123添加到elementData中
* 后续添加和扩容操作与jdk7一样
*
* 小结:jdk7中的ArrayList对象的创建类似单例模式下的饿汉式;而jdk8中的ArrayList对象的创建类似
* 于单例模式下的懒汉式,延迟了数组的创建,节省内存。
*
*
* LinkedList源码分析:
* LinkedList list = new LinkedList(); 内部声明了Node类型的first和last属性,默认值为null
* list.add(123); //将123封装在Node中,创建了Node对象
*
* 其中,Node定义为:体现了LinkedList的双向链表的说法
*
*
* Vector源码分析:
* jdk7和jdk8中通过Vector()构造器创建对象时,底层都创建了长度为10的数组。
* 在扩容方面,默认扩容为原来的数组长度的2倍
*
* @Author: fxx
* @Date: 2020/12/25 23:02
*/
public class ListTest {
@Test
public void test1(){
ArrayList arrayList = new ArrayList();
arrayList.add(123);
arrayList.add(456);
arrayList.add(new Person("Mike",12));
arrayList.add(123);
System.out.println(arrayList);
//add(int index, Object obj):在索引index处添加对象obj
arrayList.add(1,789);
System.out.println(arrayList);
//addAll(Collection coll):添加集合coll,可以加上索引
List<String> list = Arrays.asList("abc", "def");
arrayList.addAll(list);
System.out.println(arrayList);
//get(int index):获取索引index处的元素
System.out.println(arrayList.get(1));
//indexOf(Object obj):获取obj首次出现的位置,没有就返回-1
System.out.println(arrayList.indexOf(123));
System.out.println(arrayList.indexOf(159));
//Object remove(int index):移除指定index位置的元素,并返回该元素
//boolean Object remove(Object obj):移除指定obj,并返回移除是否成功
System.out.println(arrayList.remove(0));
//int lastIndexOf(Object obj):返回obj在当前集合中最后一次出现的位置
System.out.println(arrayList.lastIndexOf(123));
//Object set(int index, Object ele):设置索引index处的元素为ele
arrayList.set(1, "elc");
System.out.println(arrayList);
//List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的左闭右开子集合
List list1 = arrayList.subList(0, 3);
System.out.println(list1);
}
}
Set
HashSet
- 遍历结果与添加过程不一定一致
- 其他的详见代码中的注释
重写hashCode()
/**
* idea重写出来的hashCode方法
* 为什么要乘上31,因为一般为了减少冲突,选的数都会大一点,但也不能太大,太大就超过数组的界限了(出界),
* 一般采用的是2的n次幂,但是2的n次幂计算出来的都是偶数,偶数的运算结果,容易与其他数的运算结果相同,
* 所以一般采用的是素数:素数的因子只有素数本身和1,这样子做是为了减少计算结果冲突的概率
* 31 = 2^5 -1
*/
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age;
return result;
}
LinkedHashSet
LinkedHashSet
作为HashSet
的子类,在添加数据的同时,每个数据多加了2个指针:分别指向前驱和后继- 遍历结果与添加过程一致
- 优点:对于频繁的遍历操作,
LinkedHashSet
的效率高于HashSet
。
TreeSet
底层采用红黑树存储
说明
/**
* Set接口的类
* HashSet:作为Set接口的主要实现类;线程不安全;可以存储null值
* LinkedHashSet:作为HashSet的子类,遍历其内部数据时,可以按照添加的顺序进行遍历
* TreeSet:可以按照添加对象的指定属性,进行排序,存储的对象必须是同一类型的。
*
* Set:存储无序的,不重复的数据
* 以HashSet为例
* 无序性:不等于随机性。存储的数据在底层数组中并非按照数组索引的顺序添加,
* 而是由数据的哈希值决定的,联想哈希表存放方式,即为无序。
* 不可重复性:保证添加的元素按照equals()判断时,不会返回true。即:相同元素只能添加一个。
*
* 添加元素的过程:以HashSet为例:
* 向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值,
* 此哈希值接着通过某种算法计算出在HashSet底层数组中存放的位置(即:索引位置),
* 判断数组此位置上是否已经有元素:
* 如果没有,则元素a添加成功。---->添加成功
* 如果有,存在元素b甚至更多元素(以链表方式存在),则比较元素a与元素b的hash值:
* 如果hash值不同,那么把元素a添加进用于存储重复位置的链表;---->添加成功
* 如果hash值相同,那么需要调用元素a所在类的equals()方法:
* equals()返回true,则所添加元素a已经存在Set中,添加失败;
* equals()返回false,则把元素a添加进用于存储重复位置的链表;---->添加成功
*
* HashSet底层:数组+链表(前提:jdk7)(jdk8使用HashMap)
*
* 要求:
* 1.向Set中添加的数据,其所在类一定要重写hashCode()和equals()。
* 2.重写的hashCode()和equals()计算相同元素得出的哈希值必须一致,所以2个方法中用到的属性尽量相同。
*
*
* TreeSet
* 1.向TreeSet中添加数据,要求是相同类的对象
* 2.两种排序方式:自然排序(实现Comparable接口) 和 定制排序(Comparator)
* 如果两种同时出现,会优先使用定制排序
* 注意:
* 自然排序中:TreeSet中比较两个元素是否相同,不再使用equals()方法,而是用重写的compareTo()方法,
* 如果compareTo()返回了0,就相当于两个元素一样了
* 定制排序中:TreeSet中比较两个元素是否相同,不再使用equals()方法,而是用重写的compare()方法,
* * 如果compare()返回了0,就相当于两个元素一样了
*
* TreeSet和TreeMap采用的是 红黑树 存储结构,特点:有序,查询速度比List快
*
*
* @Author: fxx
* @Date: 2020/12/26 14:38
*/
public class SetTest {
@Test
public void test(){
TreeSet treeSet = new TreeSet();
treeSet.add(666);
treeSet.add(456);
treeSet.add(-9);
treeSet.add(0);
treeSet.add(596);
System.out.println(treeSet); //TreeSet输出是排好序的,默认从小到大
}
}
Map
Map
:存储key-value
HashMap
:作为Map的主要实现类;线程不安全,效率高;可以存储null
的key和valueLinkedHashMap
:保证在遍历map
元素时,可以按照添加的顺序实现遍历。原因:在原有的HashMap底层结构基础上,添加了一对指针,指向前驱和后继。对于频繁的遍历操作,这个类的遍历效率会高于Hashmap
TreeMap
:保证按照添加的key-value
对进行排序,实现排序遍历。(Comparator
或者Comparable
),底层使用红黑树。Hashtable
:作为古老的实现类;线程安全,效率低;不可以存储null
的key和valueProperties
:常用来处理配置文件。key和value都是String类型
HashMap
底层:- 数组+链表(jdk7及以前)
- 数组+链表+红黑树(jdk8)
Map结果的理解
Map
中的key
:无序的,不可重复的,使用Set
存储所有的key
---->key
所在的类要重写equals()
和hashCode()
(以HashMap
为例,如果是TreeMap
,则关注的是Comparator
和Comparable
)Map
中的value
:无序的,可重复的,使用Collection
存储所有的value
---->value
所在类要重写equals()
方法- 一个键值对:
key-value
构成了一个Entry
对象 Map
中的entry
:无序的,不可重复的,使用Set
存储所有的entry
常用方法
- 添加:
put(Object obj, Object value)
- 删除:
remove(Object key)
- 修改:
put(Object obj, Object value)
- 查询:
get(Object key)
- 长度:
size()
- 遍历:
keySet()
、values()
、entrySet()
Properties
public void test() throws Exception{
Properties pro = new Properties();
FileInputStream 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);
//后面才复习流,关于流的关闭等操作具体看后面流的文章
}
HashMap等其他Map(重点)
/**
* HashMap的底层实现原理? 以jdk7为例说明:
* HashMap map = new HashMap();
* 在实例化以后,底层创建了长度为16的一维数组Entry[] table。
* map.put(key1, value1):
* 首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry
* 数组中的存放位置,
* 如果此位置上没有数据,那么添加key1-value1(即Entry)--->添加成功
* 如果此位置上有数据(一个或多个,多个以链表方式存储),比较key1和已经存在的一个或多个数据
* 的哈希值:
* 如果key1的哈希值与已经存在的数据的哈希值不同,那么添加key1-value1(即Entry)--->添加成功
* 如果key1的哈希值与已经存在的数据(key2-value2)的哈希值相同,则继续比较:
* 调用key1所在类的equals(key2)
* 如果equals()返回false:那么添加key1-value1(即Entry)--->添加成功
* 如果equals()返回true:使用value1替换value2(重点!!!)
*
* 在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空时),扩容。
* 默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来,但是复制过来的时候,
* 需要重新计算原有元素的hash值,重新放到新数组里。
*
*
* jdk8相较于jdk7在底层实现方面不同:
* 1.new HashMap():底层没有创建一个长度为16的数组
* 2.jdk 8 底层的数组是:Node[],而非Entry[]
* 3.首次调用put()方法时,底层创建长度为16的数组
* 4.jdk7底层结构只有:数组+链表。jdk8中底层结构:数组+链表+红黑树。
* 5.当数组中某一位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,
* 索引位置上的所有数据改为使用红黑树存储。
*
*
* DEFAULT_INITIAL_CAPACITY:HashMap的默认容量:16
* DEFAULT_LOAD_FACTOR:HashMap的默认加载因子:0.75(决定什么时候扩容)
* threshold:扩容的临界值 = 容量 * 加载因子:16 * 0.75 = 12(已经put的个数)
* TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8
* MIN_THREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64
*
* LinkedHashMap底层原理:
* 源码中,大部分源码还是跟父类HashMap一样,只是在put的时候重写了父类的newNode方法和Node类
* 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);
* }
* }
*
* jdk8里的HashSet底层是new了HashMap,add的时候调用的是HashMap的put(key, value)方法,
* key就是add(Object obj)里的obj,而value是一个Object,没有值,只是为了防止null,而且
* 这个Object是static的,也就是说在内存里只有一份,所有的key都指向了同一个Object,节省内存
*
*
* TreeMap
* 向TreeMap中添加key-value,要求key必须是由同一个类创建的对象,因为要按照key进行排序:
* 自然排序、定制排序
* 具体参考TreeSet,方式差不多
*
*
* @Author: fxx
* @Date: 2020/12/26 20:53
*/
public class MapTest {
/**
* 元视图操作:
* Set ketSet():返回所有key构成的Set集合
* Collection values():返回所有value构成的Collection集合
* Set entrySet():返回所有key-value构成的Set集合
*/
@Test
public void test(){
HashMap hashMap = new HashMap();
hashMap.put("AA",123);
hashMap.put("BB",456);
hashMap.put("CC",789);
Set set = hashMap.keySet();
Iterator iterator = set.iterator();
while(iterator.hasNext())
System.out.println(iterator.next());
Collection values = hashMap.values();
Iterator iterator1 = values.iterator();
while(iterator1.hasNext())
System.out.println(iterator1.next());
Set set1 = hashMap.entrySet();
Iterator iterator2 = set1.iterator();
while(iterator2.hasNext())
System.out.println(iterator2.next());
}
}
Collections工具类
Collections
是一个操作Set
、List
、Map
等集合的工具类Collections
中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作,还提供了对集合对象设置不可变、对集合对象实现同步控制等方法。- 排序操作(static):
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)
:根据定制排序指定的顺序,返回给定集合中最大元素Object min(Collection)
:同上(最小)Object min(Collection, Comparator)
:同上(最小)int frequency(Collection Object)
:返回指定集合中指定元素的出现次数void copy(List dest, List src)
:将src
中的内容复制到dest
中boolean replaceAll(List list, Object oldVal, Object newVal)
:使用新值替换List
中对象的所有旧值
/**
* Collections是操作Collection和Map的工具类
* @Author: fxx
* @Date: 2020/12/27 13:46
*/
public class CollectionsTest {
@Test
public void test(){
ArrayList list = new ArrayList();
list.add(123);
list.add(46);
list.add(765);
list.add(-97);
list.add(0);
System.out.println(list);
//copy的错误用法
// List list1 = new ArrayList();
// Collections.copy(list1,list); //这样会报异常,因为list1的长度必须>=list才能复制
//copy正确的用法
List list1 = Arrays.asList(new Object[list.size()]); //把数组长度撑起来
System.out.println(list1.size());
Collections.copy(list1, list);
System.out.println(list1);
ArrayList arrayList = new ArrayList();
List list2 = Collections.synchronizedList(arrayList);
}
}
集合类的线程安全问题
Collections
类中提供了多个synchronizedXxx()
方法,该方法可将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题
//返回的list2就是线程安全的
ArrayList arrayList = new ArrayList();
List list2 = Collections.synchronizedList(arrayList);
面试题
1
ArrayList、LinkedList、Vector
三者的异同?
- 相同:
- 三个类都实现了
List
接口,存储数据的特点相同:存储有序的,可重复的数据。
- 三个类都实现了
- 不同:
- 见上面源码部分
2
@Test
public void test(){
List list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
updateList(list);
System.out.println(list); //1,2
}
public void updateList(List list){
list.remove(2); //这里remove的参数是索引,不是值
// list.remove(new Integer(2)); //要删掉值的话new对象
}
3
//Person类已经重写equals()和hashCode()
@Test
public void test(){
HashSet set = new HashSet();
Person p1 = new Person(1001, "AA");
Person p2 = new Person(1002, "BB");
set.add(p1);
set.add(p2);
System.out.println(set); //1001,AA,1002,BB
p1.name = "CC"; //这一步修改了原来的AA为CC,但hashCode值依然是用AA计算出来的
set.remove(p1);
System.out.println(set); //1001,CC,1002,BB
//所以下面这步添加的时候,计算出的CC的hashCode值很可能跟AA的不一样,所以是可以添加成功的
set.add(new Person(1001, "CC")); //1001,CC,1002,BB,1001,CC
System.out.println(set);
//而这一步,因为原来的AA已经变成了CC,这里虽然用新AA计算出的hashCode值和原来的AA一样了,但是用equals()比较发现值不一样,AA不等于CC,所以新AA还是可以添加成功
set.add(new Person(1001, "AA")); //1001,CC,1002,BB,1001,CC,1001,AA
System.out.println(set);
}
4
HashMap
底层实现原理?
见源码分析
5
HashMap
和Hashtable
异同?
见源码分析
6
CurrentHashMap
与 Hashtable
异同?
7
Collection
和Collections
的区别?
后者是操作前者的工具类