Java类集

1 Collection集合接口

存储单值的顶级接口。该接口下最常用的一些方法:

  • public boolean add​(E e) 添加元素;
  • public boolean addAll​(Collection<? extends E> c) 添加所有元素
  • public boolean clear() 清空集合,让根节点为空。
  • public boolean contains(Object o) 查询数据是否存在。需要equals方法支持。
  • public int size() 获取数据长度
  • public Object[] toArray() 将集合变为对象数组返回
  • public Iterator<E> iterator() 获得集合的iterator
  • public <T> T[] toArray​(T[] a) 将集合变为指定类型的对象数组返回
  • public boolean remove​(Object o) 删除元素

在JDK1.5之前,通常都是操作的Collection接口。在JDK1.5之后,应该操作Collection的两个子接口:

  • List接口:允许数据重复
  • Set接口:不允许数据重复

在这里插入图片描述

2 List接口

List接口对Collection接口的方法进行了扩充。

  • public void add(int index, E element) 在指定位置添加数据
  • public E get(int index) 得到指定下标的数据
  • public E set(int index, E element) 将指定下标的数据修改
  • public int indexOf​(Object o) 获得元素的下标
  • public ListIterator<E> listIterator() 返回ListIterator接口对象。

在JDK 9之后,List增加了一系列的静态方法,可以快速创建List。
public static <E> List<E> of​(E... elements)

List有三个子类:

  • ArrayList(90%):
  • LinkedList(8%):
  • Vector(2%):

2.1 ArrayList子类

继承关系
在这里插入图片描述

  • ArrayList本质上是基于数组实现的(这也是为什么可以通过set(int index, E element)和get(int index)等操作下标的方法操作)。
  • 如果使用ArrayList的无参构造方法,会默认构造一个空数组。在进行第一次数据追加时,比较增加的数据长度默认长度的大小关系,取较大的一个数值作为容量进行新数组开辟。注意:默认长度为10。

在JDK1.9之后:
ArrayList默认的构造只会使用默认的空数组,在进行第一次数据追加时才会开辟数组,默认的开辟长度为10。
在JDK1.9之前:
ArrayList默认的构造实际上就会开辟大小为10的数组。

  • 如果调用有参构造方法初始化initialCapacity,则会构造一个该容量大小的数组

  • 如果在进行数据追加时发现数组长度不够,则会进行新数组开辟并且将原数组数据拷贝到新数组中。数组长度的增长量是数组长度的1/2。假设原始长度为10,则下一次增长量为5(即新数组长度为15).

int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
  • 在使用ArrayList的时候一定要估算你的数据量,如果超过10个则使用有参构造创建,避免垃圾数组空间产生。

2.2 LinkedList子类

Linkedlist是一个双向链表,除了常规的add, set, get, remove方法之外,还提供了一系列***First, ***Last方法,用于对头部或尾部进行操作。例如:addFirst, removeFirst, getLast等等。
此外,这个类还提供了栈的相关操作,例如push,pop等。
继承关系
在这里插入图片描述

如果只观察功能会发现LinkedList和ArrayList是一样的。但两者在实现上有很大区别。

  • LinkedList是基于链表实现的
  • LinkedList不提供具有初始容量的有参构造方法(只有无参构造方法)
  • 所有数据保存在Node类里。
  • add方法可以添加null
  • add方法添加数据时,直接追加到链表末尾(通过linkLast方法)。为了提高效率,在LinkedList类之中始终保存最后一个节点,这样避免了追加操作时大量的递归操作。

ArrayList和LinkedList有什么区别?

  • 前者基于数组实现,后者基于链表实现;
  • 通过get(int index)方法获取数据时,ArrayList的时间复杂度为O(1),LinkedList的时间复杂度为O(n)
  • ArrayList默认的初始化数组长度为10,当数组长度不够时进行动态扩容,保存大数据时可能会造成垃圾的产生与性能下降。这个时候可以使用LinkedList。

2.3 Vector子类

这个类从JDK1.0就出现了。考虑到JDK1.2之后很多开发者已习惯了Vector,且很多系统也使用了这个类,因此Java将其保留了下来。

从定义上来看Vector类和ArrayList类是一样的。
在这里插入图片描述

Vector类如果使用无参构造方法,则一定会开辟一个长度为10的数组。而后其余的操作和ArrayList是相同的。

Vector和ArrayList的区别:
Vector类中的操作方法都是使用synchronized进行同步处理,而ArrayList之中没有。因此Vector类之中的方法在多线程访问过程中是线程安全的,但性能比ArrayList低。

3 Set接口

Set接口不能像List接口那样操作Index。
JDK1.9之后Set接口也增加了许多的of静态方法进行实例化。当使用of方法保存有重复元素时,会抛出异常。

为Set接口常规的实例化是通过子类实现的。两个重要子类:HashSet和TreeSet。

3.1 HashSet子类

用的最多的子类,最大的特点是保存的数据是无序的。
继承结构:
在这里插入图片描述

3.2 TreeSet子类

与HashSet的不同是TreeSet保存的数据是有序的。添加数据时默认按照数据升序排列。
在这里插入图片描述

TreeSet保存自定义类对象时需要实现Comparable接口。覆写compareTo方法时需要注意:必须将类的所有属性进行比较,否则当两个对象的一个或几个属性相同时,TreeSet会认为两者是相同的从而造成部分数据不能正确存入。
结论:TreeSet依靠compareTo方法确认重复

由于TreeSet需要让数据对象的compareTo方法考虑所有的类属性,当类属性很多时非常麻烦,因此开发中首选HashSet保存数据。

3.3 重复元素说明

在Java程序之中真正重复元素的判断是利用hashCode()和equals()方法共同判断的,只有在有排序的情况下(比如TreeSet)才会利用Comparable接口来实现。

4 输出

4.1 Iterator迭代输出(95%)

通过Iterable接口的iterator()方法进行实例化。
在迭代输出的时候如果使用了Collection类提高的remove方法会造成并发更新异常,导致程序异常。此时应该使用iterator.remove()方法进行删除。

4.2 ListIterator双向迭代输出(0.1%)

Iterator只允许从前向后单向输出,这个类可以进行双向输出。这个类在Collection类中没有实例化方法,但在List子接口中有。换句话说,这个类是专门为List的子类设计的。
两个方法:hasPrevious()previous()
这个实现是类似于指针的。因此想实现从后向前输出,必须先进行从前向后输出。

4.3 Enumeration枚举输出

JDK1.0就有了,这个输出接口只为Vector一个类服务。
Enumeration类对象通过Vector中的elements()方法实例化。
遍历Enumeration对象时类似于Iterator方法。

Enumeration<String> enu = v.elements();
while (enu.hasMoreElements()) {
	System.out.println(enu.nextElement());
}

继承结构:
在这里插入图片描述

4.4 ForEach输出(4.9%)

for(String str: list){
...
}

5 Map接口

保存二元偶对象的最大父接口。(Collection只能保存单个对象)。
在开发里面,Collection集合保存数据的目的是为了输出,Map集合保存数据的目的是为了进行key的查找。

Map接口简介。
该接口为一个独立的父接口,在接口对象实例化时需要设置K和V。Map里有以下的操作方法需要记住。

  • public V put(K key, V value);
  • public V get(Object key);

思考:为什么get方法里的key是Object类型?
因为Map在查找数据时是找到Key相同的Entry,这里的key比较时调用的正是Object类中提供的equals和hashCode方法,因此用Object类型。
其次,如果指定Key为K类型,考虑如下的代码

class S<K>{
	public void contains(K k){
		System.out.println("S<K>,contains(K k)");
	}
}
class Foo{
}

class SubFoo extends Foo{
}
	
public void doSomeReading(S<? extends Foo> foos) { 
	Foo f = new Foo();
	SubFoo subFoo = new SubFoo();
	foos.contains(f); //这里eclipse会提示出错,这里只有填null时才不会报错
	foos.contains(subFoo); //同样错误
	foos.contains(null);
}

原来在S<K>的定义中,我们明确contains(K k)函数只能接受一个明确类型的参数。但是在doSomeReading函数中,编译器无法确定到底是什么类型,它是Foo类型,还是SubFoo类型,还是SubSubFoo类型?编译器无从得知,所以它只允许null类型的参数。

  • public Set<Map.Entry<K,​V>> entrySet() 将Map集合转成Set集合。
  • public Set<K> keySet() 获得Key的set集合

由此可见,Map中的key是不可能重复的。

从JDK1.9之后,Map接口里也提供了一系列静态的of方法供用户使用。该方法如果传入重复的key则会报异常:IllegalArgumentException,如果传入null则会报异常:NullPointerException。

正常使用Map应该使用其子类实例化。

5.1 HashMap子类

该类的主要特点是无序存储。(Hash —— 无序,Tree——有序)。存储的时候采用对象数组+链表/红黑树的形式保存。对象数组的每一个位置代表一个桶。对象的Hash码经过一系列计算对应到对象数组的某一个桶下。

在这里插入图片描述

HashMap采⽤Entry数组来存储key-value对,每⼀个键值对组成了⼀个Entry实体,Entry类实际上是⼀个单向的链表结 构,它具有Next指针,可以连接下⼀个Entry实体。 只是在JDK1.8中,链表⻓度⼤于8的时候,链表会转成红⿊树!

  • 第一问: 为什么使用链表+数组:要知道为什么使用链表首先需要知道Hash冲突是如何来的:
    答: 由于我们的数组的值是限制死的,我们在对key值进行散列取到下标以后,放入到数组中时,难免出现两个key值不同,但是却放入到下标相同的格子中,此时我们就可以使用链表来对其进行链式的存放。
  • 第二问 我⽤LinkedList代替数组结构可以吗?
    对于题目的意思是说,在源码中我们是这样的
    java Entry[] table=new Entry[capacity]; // entry就是一个链表的节点
    现在进行替换,进行如下的实现
    java List<Entry> table=new LinkedList<Entry>();
    是否可以行得通? 答案当然是肯定的。
  • 第三问 那既然可以使用进行替换处理,为什么有偏偏使用到数组呢?
    因为⽤数组效率最⾼! 在HashMap中,定位节点的位置是利⽤元素的key的哈希值对数组⻓度取模得到。此时,我们已得到节点的位置。显然数组的查找效率⽐LinkedList⼤(底层是链表结构)。
    那ArrayList,底层也是数组,查找也快啊,为啥不⽤ArrayList? 因为采⽤基本数组结构,扩容机制可以⾃⼰定义,HashMap中数组扩容刚好是2的次幂,在做取模运算的效率⾼。 ⽽ArrayList的扩容机制是1.5倍扩容(这一点我相信学习过的都应该清楚),那ArrayList为什么是1.5倍扩容这就不在本⽂说明了。

5.1.1 Hash冲突:得到下标值:

我们都知道在HashMap中 使用数组加链表,这样问题就来了,数组使用起来是有下标的,但是我们平时使用HashMap都是这样使用的:

HashMap<Integer,String> hashMap=new HashMap<>();
hashMap.put(2,"dd");

可以看到的是并没有特地为我们存放进来的值指定下标,那是因为我们的hashMap对存放进来的key值进行了hashcode(),生成了一个值,但是这个值很大,我们不可以直接作为下标,此时我们想到了可以使用取余的方法,例如这样:
key.hashcode()%Table.length;
即可以得到对于任意的一个key值,进行这样的操作以后,其值都落在0-Table.length-1 中,但是 HashMap的源码却不是这样做?
它对其进行了与操作,对Table的表长度减一再与生产的hash值进行相与:

if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

我们来画张图进行进一步的了解。

在这里插入图片描述

这里我们也就得知为什么Table数组的长度要一直都为2的n次方,只有这样,减一进行相与时候,才能够达到最大的n-1值。
举个栗子来反证一下:
我们现在 数组的长度为 15 减一为 14 ,二进制表示 0000 1110 进行相与时候,最后一位永远是0,这样就可能导致,不能够完完全全的进行Table数组的使用。违背了我们最开始的想要对Table数组进行最大限度的无序使用的原则,因为HashMap为了能够存取高效,要尽量较少碰撞,就是要尽量把数据分配均匀,每个链表⻓度⼤致相同。
此时还有一点需要注意的是: 我们对key值进行hashcode以后,进行相与时候都是只用到了后四位,前面的很多位都没有能够得到使用,这样也可能会导致我们所生成的下标值不能够完全散列
解决方案:将生成的hashcode值的高16位与低16位进行异或运算,这样得到的值再进行相与,一得到最散列的下标值。

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

在这里插入图片描述

5.1.2 保存数据put方法。

使用HashMap保存数据时(put方法),可以保存null数据(key或value都可以为null)
当存入重复key时,会将新的Value覆盖掉旧的Value。同时返回的旧的Value
存入不重复的key时,返回null。

5.1.3 分析源码

(a) 构造方法

public HashMap(){
	this.loadFactor = DEFAULT_LOAD_FACTOR;
}

当使用无参构造的时候,会出现有一个loadFactor的属性,默认值是DEFAULT_LOAD_FACTOR = 0.75f

(b) put方法

public V put(K key, V value){
	return putVal(hash(key), key, value, false, true);
}

在使用put方法进行数据保存时,会调用一个putVal方法,对key进行hash处理。
putVal方法里提供一个Node节点类进行数据保存。在这个方法里还调用了一个resize方法实现数据扩容。

面试题一:在进行HashMap的put操作时,如何实现容量扩充的?

  • 首先HashMap定义了一个初始化容量DEFAULT_INITIAL_CAPACITY = 16。即默认初始化容量为16。
  • 当数据量达到 容量 * 阈值(DEFAULT_LOAD_FACTOR)的时候(第一次扩充的时候即达到16 * 0.75 = 12),就会进行容量的扩充。
  • 采用成倍模式扩容。即新的容量是旧容量的2倍。

面试题二:请解释HashMap的工作原理(JDK1.8之后开始的,因为1.8之后考虑到海量大数据的保存)。
在HashMap之中进行数据存储的依然是Node类。那么这种情况下只能使用两种结构:链表或二叉树。
当数据不超过8个时,采用链表保存;超过8个后,将链表转为红黑树并通过左旋右旋保证查询的性能。当数据量减少到6时,会从红黑树转为链表

5.2 LinkedHashMap子类

HashMap本身保存数据是无序的(有序与否对Map没有影响)。如果希望保存的数据是按照其增加的顺序保存,则可以采用LinkedHashMap子类(基于链表实现)。既然是链表保存,则尽量不要保存特别大的数据量。

在开发中首选HashMap,如果确定了数据保存量则可以使用LinkedHashMap。

5.3 Hashtable子类

Hashtable是从JDK1.0开始提供的,与Vector、Enumeration属于最早一批的动态数组实现类。
在这里插入图片描述

Hashtable与HashMap的区别:

  • HashMap中的方法都属于异步操作(非线程安全),可以保存null数据(key和Value都可以为null)。
  • Hashtable中的方法都属于同步方法(线程安全),保存key和value都不能为空

5.4 TreeMap子类

TreeMap属于有序的Map集合类型,它可以按照key进行排序。这时Key需要实现Comparable接口。
在这里插入图片描述

注意:TreeMap不可以保存key为null的数据。

6 Map.Entry内部接口

对于List(LinkedList)而言,数据通过Node节点类按链表形式保存。而在HashMap里虽然也可以见到Node内部类定义,但通过源码可以发现Node内部类本质上实现了Map.Entry接口。

static class Node<K, V> implements Map.Entry<K, V> {}

所以可以得出结论:所有的key和value数据都被封装在Map.Entry内部接口中。该接口提供了两个重要方法。

  • public K getKey();
  • public V getValue();

在这里插入图片描述

7 Iterator输出Map集合

首先,Map接口中并没有提供返回iterator的方法。
有以下几种方法:
一、先得到keySet,然后通过get(K key)方法输出value;
二、返回Map.Entry的set,然后通过Iterator遍历该set。

8 Hash冲突

  • 使用put方法保存数据时,会先将key做一个hash处理,最后保存的数据也保存了这个hash值。
  • 使用Get方法取出数据时,同样会先将传入的key做hash处理获得对应的hash码,在匹配的过程中只有key满足hash值相等且equals为true时才能实现匹配。

因此,如果想要自定义Key类型,必须要覆写hashCode方法和equals方法(通过IDE可自动生成)。

面试题:当使用HashMap进行数据保存时,出现Hash冲突(HashM码相同)时Hash Map会如何处理?
在这里插入图片描述

为了保证数据都能保存下来,使用链表来保存数据。

9 Stack栈

Stack类是Vector的子类,但并不采用Vector提供的方法,它有自己的入栈和出栈操作:

  • E push(E element)
  • E pop()

10 Queue队列

在这里插入图片描述
在这里插入图片描述

Queue的实现可以通过LinkedList完成。

  • boolean offer(E e)
  • E poll()
    另外还可以通过PriorityQueue实现Queue,PriorityQueue具有排序功能,可以将元素按照compareTo定义的顺序取出。

11 Properties属性操作

在国际化程序一节,我们提到了*.properties文件,它是以"key=value"的形式保存的数据,这和Map很像。唯一的区别在于其保存的内容只能是字符串。在java.util包里面提供了一个Properties类,此类是Hashtable的子类。

public class Properties extends Hashtable<Object, Object>

可以发现在继承Hashtable的时候,Hashtable定义泛型为Object。Properties是不需要操作泛型的,因为其只能操作String。

  • String setProperty(String key, String value) (不用关注返回值)设置属性
  • String getProperty(String key) 返回属性,不存在时返回Null
  • String getProperty(String key, String defaultValue) 返回属性,不存在时返回默认值
  • void store(OutputStream out, String comments) throws IOException 将属性输出
  • void load(InputStream inStream) throws IOException 将属性加载到Properties对象

Properties操作和Map很像,为什么要单独列出这个类?因为它还有额外的属性存储和加载的方法。在实际开发中,Properties通常用于读取配置资源的信息。ResourceBundle读取的是信息资源文件。

12 Collections工具类

这个类可以操作Map,List,Set和Queue。
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值