文章目录
集合数组和集合的区别?集合框架图:Iterator接口:Collection接口:遍历元素的方式
List接口:List的三种遍历方式ArrayList(不是线程安全的)LinkedList(不是线程安全的)ArrrayList和LinkedList的区别?Vector底层结构和ArrayList的比较:
Set接口:HashSet添加元素的流程
LinkedHashSet
Map接口:HashMap(不是线程安全)HashTable(是线程安全的)LinkedHashMap
开发中如何选择集合实现类
集合
数组和集合的区别?
集合可以动态保存任意多个对象,使用比较方便,数组是固定长度的数据结构集合提供了一系列方便的操作对象的方法: add、remove、 set、 get,数组则直接访问元素数组可以包含基本数据类型和对象,而集合只能包含对象。
集合框架图:
//1.集合主要分两组(单列集合,双列集合)
//2.Collection接口有两个重要的子接口 List Set,它们的实现子类都是单列集合(集合中存放单个元素)
//3.Map接口实现子类属于双列集合,存放的k-v
Collection:
Map:
Iterator接口:
collection继承了iterator接口,任何集合类都可以用增强for循环遍历!
public interface Iterator
boolean hasNext();//是否存在下一个
E next();//逐个访问集合中元素
default void remove() {//删除上一个next的元素
throw new UnsupportedOperationException("remove");
}
default void forEachRemaining(Consumer super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
Collection接口:
collection实现子类可以存放多个元素,每个元素可以是Object
有些Collection的实现类,可以存放重复的元素,有些不可以
有些Collection的实现类,有些是有序的(List), 有些不是有序(Set)
Collection接口没有直接的实现子类,是通过它的子接口Set和List来实现的
public abstract class AbstractCollection
遍历元素的方式
方式1-使用Iterator迭代器
//hasNext()//判断是否有下一个元素
while(iterator.hasNext()){
//next()作用:1.下移 2.将下移以后集合位置上的元素返回
}
Collection col = new ArrayList();
col.add(new Book("三国演义","罗贯中",10.9));
col.add(new Book("红楼梦","曹雪芹",9.8));
col.add(new Book("活着","余光华",6.6));
//System.out.println("col="+col);
//逐个遍历
//1.先得到col对应的迭代器
Iterator iterator = col.iterator();
//2.使用while循环遍历
while (iterator.hasNext()) {//判断是否还有数据
//返回下一个元素,类型是Object
Object obj = iterator.next();
System.out.println("obj=" + obj);//看obj的运行类型
}
//快捷键,快速生成while循环:itit
//显示所有的快捷键的快捷键 ctrl+j
//3.当退出while循环后,这时iterator迭代器,指向最后的元素
//iterator.next();//NoSuchELementException
//4.如果希望再次遍历,需要重置迭代器
iterator = col.iterator();
System.out.println("===第二次遍历===");
while (iterator.hasNext()) {
Object obj = iterator.next();
System.out.println("obj=" + obj);
}
方式2-增强for循环遍历
Collection col = new ArrayList();
col.add(new Book("三国演义","罗贯中",10.9));
col.add(new Book("红楼梦","曹雪芹",9.8));
col.add(new Book("活着","余光华",6.6));
//1.使用增强for,在 Collection集合
//2.增强for,底层仍然是iterator
//3.增强for可以理解成简化版本的迭代器遍历
//4.快捷方式I
for (Object book:col) {
System.out.println("book=" + book);
}
List接口:
//List集合类中元素有序(及添加顺序和取出顺序一致),且可重复
List list = new ArrayList();
list.add("猪八戒");
list.add("哪吒");
list.add("金刚葫芦娃");
list.add("大头儿子");
System.out.println("list="+list);
//2.List集合中的每个元素都有其对应的顺序索引,即支持索引
//索引是从0开始的
System.out.println(list.get(3));
//3.操作元素的一些方法
//1) void add(int index, Object ele):在index位置插入ele元素
//2) boolean addAll(int index, Collection eles):从index位置开始将
//eles中的所有元素添加进来
//3) Object get(int index):获取指定index位置的元素
//4) int indexOf(Object obj):返回obj在集合中首次出现的位置
//5) int lastIndexOf(Object obj):返回obj在当前集合中未次出现的位置
//6) Object remove(int index):移除指定index位置的元素,并返回此元
//素
//7) Objeqt set(int index, Object ele);:设置指定index位置的元素为ele,
//相当于是替换.
//8) List subList(int fromIndex, int tolndex):返回从fromIndex到
//tolndex位置的子集合
List的三种遍历方式
方式1-使用迭代器
Iterator iterator= list.iterator();
while (iterator.hasNext()) {
Object object = iterator.next();
System.out.println("object="+object);
}
方式2-增强for循环
for (Object object : list) {
System.out.println("object="+object);
}
方式3-普通for循环
for (int i = 0; i < list.size(); i++) {
System.out.println("object="+list.get(i));
}
ArrayList(不是线程安全的)
ArrayList中维护了一个0bject类型的数组elementData. [debug 看源码] transient Object[] elementData; //transient表示瞬间,短暂的,表示该属性不会被序列化
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//执行list.add (1)先确定是否要扩容 (2)然后在执行赋值
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
//private static final int DEFAULT_CAPACITY = 10;
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);//该方法确定minCapacity(1)第一次扩容为10
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;//记录集合被修改的次数
// overflow-conscious code
if (minCapacity - elementData.length > 0)//如果elementDate的大小不够,就调用grow()去扩容
grow(minCapacity);
}
//使用扩容机制来确定要扩容到多大
//第一次newCapacity = 10
//第二次及其以后,按照1.5倍扩容
//扩容使用的是Arrays.copyof()
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData容量为0, 第1次添加,则扩容elementData为10,如需要再次扩容,则扩容elementData为1 .5倍。如果使用的是指定大小的构造器,则初始elementData容量为指定大小, 如果需要扩容,则直接扩容elementData为1.5倍。
LinkedList(不是线程安全的)
public class LinkedList
extends AbstractSequentialList
implements List
{
//...
}
LinkedList 实现了以下接口:
LinkedList 中的元素是通过 Node 定义的:
private static class Node
E item;// 节点值
Node
Node
// 初始化参数顺序分别是:前驱结点、本身节点值、后继节点
Node(Node
this.item = element;
this.next = next;
this.prev = prev;
}
}
遍历linkedlist方法:
public class Main {
public static void main(String[] args) {
//add()和remove()方法在失败的时候会抛出异常(不推荐)
Queue
//添加元素
queue.offer("a");
queue.offer("b");
queue.offer("c");
queue.offer("d");
queue.offer("e");
for(String q : queue){
System.out.println(q);
}
System.out.println("===");
System.out.println("poll="+queue.poll()); //返回第一个元素,并在队列中删除
for(String q : queue){
System.out.println(q);
}
System.out.println("===");
System.out.println("element="+queue.element()); //返回第一个元素
for(String q : queue){
System.out.println(q);
}
System.out.println("===");
System.out.println("peek="+queue.peek()); //返回第一个元素
for(String q : queue){
System.out.println(q);
}
}
ArrrayList和LinkedList的区别?
是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
底层数据结构:ArrayList 底层使用的是 Object 数组;LinkedList 底层使用的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!)
插入和删除是否受元素位置的影响:
ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element)),时间复杂度就为 O(n)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。
LinkedList 采用链表存储,所以在头尾插入或者删除元素不受元素位置的影响(add(E e)、addFirst(E e)、addLast(E e)、removeFirst()、 removeLast()),时间复杂度为 O(1),如果是要在指定位置 i 插入和删除元素的话(add(int index, E element),remove(Object o),remove(int index)), 时间复杂度为 O(n) ,因为需要先移动到指定位置再插入和删除。
是否支持快速随机访问:LinkedList 不支持高效的随机元素访问,而 ArrayList(实现了 RandomAccess 接口) 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。
内存空间占用:ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。
我们在项目中一般是不会使用到 LinkedList 的,需要用到 LinkedList 的场景几乎都可以使用 ArrayList 来代替,并且,性能通常会更好!就连 LinkedList 的作者约书亚 · 布洛克(Josh Bloch)自己都说从来不会使用 LinkedList
Vector底层结构和ArrayList的比较:
Set接口:
元素是无序的,查找速度快。
HashSet
实际上是HashMap
public HashSet() {//HashSet的构造器
map = new HashMap<>();
}
2.可以存放null值,但是只能有一个null
3.不保证元素有序性,取决于hash后,在确定索引的结果。
4.不能有重复的元素。
添加元素的流程
HashSet hashSet = new HashSet();
hashSet.add("java");
//1.执行HashSet()
public HashSet() {
map = new HashMap<>();
}
//2.执行add()
public boolean add(E e) {//e="java"
return map.put(e, PRESENT)==null;
//private static final Object PRESENT = new Object();
}
//3.执行put(),该方法会执行hash(key) 得到key对应的hash值 算法h = key.hashCode() ^ (h >>> 16) 无符号右移动16位
public V put(K key, V value) {//key = "java" value = PRESENT 共享
return putVal(hash(key), key, value, false, true);
}
//4.执行putVal()
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node
//table就是HashMap的一个数组,类型是Node[]
//if语句表示如果当前table是null,或者大小=0
//就是第一次扩容,到16个空间
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//(1)根据key,得到hash 去计算该key应该存放到table表的哪个索引位置,并把这个位置的对象,赋给p
//(2)判断p 是否为null
//(2.1)如果p 为null,表示还没有存放元素,就创建一个Node(key = "java" value = PRESENT)
//(2.2)就放在该位置tab[i] = newNode(hash,key,value,null)
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//在需要辅助变量的时候,再创建。
Node
//如果当前索引位置对应的链表的第一个元素和准备添加的key的hash值一样
//并且满足下面两个条件之一:
//(1)准备加入的key 和 p指向的Node结点的Key是同一个对象
//(2)p指向的Node结点的Key的equals()和准备加入的key比较后相同 就不能加入
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//再判断p是不是一颗红黑树,如果是一颗红黑树,就调用putTreeVal,来进行添加
else if (p instanceof TreeNode)
e = ((TreeNode
else {//如果table对应索引位置,已经是一个链表,就使用for循环比较
//(1)依次和该链表的每一个元素比较后,都不相同,则加入到该链表的最后,注意在把元素添加到链后,立即判断,该链表是否已经达到8个结点,就调用treefyBi(),对当前这个链表进行树化(转成红黑树),注意,在转成红黑树时,要进行判断,判断条件
//(2)依次和该链表的每一个元素比较过程中,如果有相同情况,就直接break
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {//如果整个链表,没有和它相同,就加到该链表的最后
p.next = newNode(hash, key, value, null);
//加入后,判断当前链表的个数,是否已经到8个,到8个后,就调用treeifyBin进行红黑树转换
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&//如果在循环比较过程中,发现有相同,就break,只是替换value
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;//替换key对应的值
afterNodeAccess(e);
return oldValue;
}
}
++modCount;//每增加一个Node,就size++
if (++size > threshold)//12->24->24如果size大于临界值就扩容
resize();//扩容
afterNodeInsertion(evict);
return null;
}
HashSet底层是HashMap,第一次添加时,table 数组扩容到16,临界值(threshold)是16*加载因子(loadFactor)是0.75 = 12如果table数组使用到了临界值12,就会扩容到16* 2 = 32,新的临界值就是32*0.75 = 24,依次类推在Java8中,如果一条链表的元素个数到达TREEIFY THRESHOLD(默认是8 ),并且table的大>=MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制、
练习:定义Employee类,属性有姓名和年龄,加入到hashset中,如果姓名和年龄相等,则不加入。
public class HashSetExercise_ {
public static void main(String[] args) {
Employee jack = new Employee("jack", 10);
Employee mark = new Employee("jack", 10);
Employee june = new Employee("june", 15);
HashSet hashSet = new HashSet();
hashSet.add(jack);
hashSet.add(mark);//加入不了,因为姓名和年龄存在
hashSet.add(june);
System.out.println(hashSet);
}
}
class Employee {
private String name;
private int age;
public Employee(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee employee = (Employee) o;
return age == employee.age && Objects.equals(name, employee.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
LinkedHashSet
LinkedHashSet是HashSet的子类LinkedHashSet底层是一个LinkedHashMap,底层维护了一个数组+双向链表LinkedHashSet根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序(图),这使得元素看起来是以插入顺序保存的。LinkedHashSet不允许添重复元素
Map接口:
Map 与Collection并列存在。用于保存具有映射关系的数据:Key-VaLue(双列元素)Map中的key和vaLue 可以是任何引用类型的数据,会封装到HashMap$Node 对象中Map中的key不允许重复,原因和HashSet一样,前面分析过源码。Map中的vaLue 可以重复Map的key可以为null,value也可以为null,注意key为null,只能有一个,value为null可以有多个。常用String类作为map的key,但并不是只能用String。key和value之间存在单向一对一关系,即通过指定的key总能找到对应的value。Map存放数据的key-value示意图,一对k-v是放在一个HashNode中的, 有因为Node实现了Entry 接口,有些书上也说一对k-v就是一个Entry。
Map hashMap = new HashMap();
hashMap.put("no1", "张无忌");
hashMap.put("no2", "赵敏");
//1. k-v最后是HashMap$Node node = newNode (hash, key, vaLue, nuLl)
//2. k-v为了方便程序员的遍历,还会创建EntrySet 集合,该集合存放的元素的类型Entry, 而一个Entry
// 对象就有k,v EntrySet
//3.在entrySet中,定义的类型是Map.Entry,但是实际上存放的还是HashMap$Node
// 这是因为HashMap$Node implements Map.Entry
//4.当把HashMap$Node对象存放到entrySet就方便我们的遍历,因为Map.entry提供一个K getKey();
// 还提供一个V getValue();
Set set = hashMap.entrySet();
System.out.println(set.getClass());//HashMap$EntrySet
for (Object object : set) {
//System.out.println(entry.getClass());//HashMap$Node
//为了从HashMap$Node 取出K-v
//1.先做一个向下转型
Map.Entry entry = (Map.Entry) object;
System.out.println("key=" + entry.getKey() + " value=" + entry.getValue());
}
HashMap(不是线程安全)
扩容机制和HashSet一样
HashMap底层维护了Node类型的数组table,默认为null当创建对象时,将加载因子(loadfactor)初始化为0.75.当添加key-val时,通过key的哈希值得到在table的索引.然后判断该索引处是否有元素,如果没有元素直接添加。如果该索引处有元素,继续判断该元素的key是否和准备加入的key相等,如果相等,则直接替换val;如果不相等需要判断是树结构还是链表结构,做出相应处理。如果添加时发现容量不够,则需要扩容。第1次添加,则需要扩容table容量为16,临界值(threshold)为12.以后再扩容,则需要扩容table容量为原来的2倍,临界值为原来的2倍即24,依次类推在Java8中,如果一条链表的元素个数超过 TREEIFY THRESHOLD(默认是8),并且table的大小>= MIN TREEIFY CAPACITY(默认64),就会进行树化(红黑树)
HashTable(是线程安全的)
存放的元素是键值对:即K-Vhashtable的键和值都不能为null,否则会抛出NullPointerExceptionhashTable使用方法基本上和HashMap-样Hashtable是通过使用了 synchronized 关键字来保证其线程安全。简单看下底层结构
//1.底层有数组Hashtable$Entry[] 初始化大小为11
//2. 临界值threshold 8 = 11 * 0.75
//3.扩容:按照自己的扩容机制来进行即可
Hashtable hashtable = new Hashtable();
hashtable.put("中国","chinese");
//hashtable.put(null,"Japan");//Exception in thread "main" java.lang.NullPointerException
//hashtable.put("usa",null);//Exception in thread "main" java.lang.NullPointerException
LinkedHashMap
LinkedHashMap是Map接口的哈希表和链接列表实现,具有可预知的迭代顺序。
LinkedHashMap实现HashMap+LinkedList
public class LinkedHashMap
extends HashMap
implements SequencedMap
{ }
初始化:
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
public LinkedHashMap() {
super();
accessOrder = false;
}
public LinkedHashMap(Map extends K, ? extends V> m) {
super();
accessOrder = false;
putMapEntries(m, false);
}
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
开发中如何选择集合实现类
在开发中,选择什么集合实现类,主要取决于业务操作特点,然后根据集合实现类特性进行选择,分析如下:
1)先判断存储的类型(一组对象[单列]或组键值对[双列])
2)一组对象: Collection接口
允许重复: List
增删多: LinkedList [底层维护了一个双向链表]
改查多: ArrayList [底层维护Object类型的可变数组]
不允许重复: Set
无序: HashSet [底层是HashMap,维护了一个哈希表即(数组+链表+红黑树)]
排序: TreeSet
插入和取出顺序一致: LinkedHashSet , 维护数组+双向链表
3)一组键值对: Map,
键无序: HashMap [底层是:哈希表jdk7:数组+链表,jdk8: 数组+链表+红黑树]
键排序: TreeMap
键插入和取出顺序一致: LinkedHashMap
读取文件Properties