Java认识泛型与容器
一、泛型
“泛型”的意思是“适用于许多许多的类型”,实现了参数化类型的概念。其最初的目的是希望类或方法具备最广泛的表达能力,通过解耦类或方法与所使用的类型之间的约束。不用像参数是类或接口那样对程序有过多约束(方法的参数不必仅限于一种类或接口与它们的子类)
使用泛型,具体来说,在定义一个类的时候,类名后面加上<T>这个类型参数,那么在类中,可以用T来表示不特定的数据类型。在创建该泛型实例时,将T换成你所需要的具体的数据类型。这个数据类型不能是基本类型。
class A<T> {
private T v;
public T get(){ return T;}
}
......
A<Integer> temp = new A<Integer>;
Integer result = temp.get();
告诉编译器想使用什么类型,然后编译器帮你处理一切细节。
import java.util.*; public class C{ public static void main(String[] args){ List list1 = new ArrayList(); list1.add("heheh"); list1.add(35.5); for(int i = 0; i < list1.size();i++){ Object s = list1.get(i); System.out.println(s); } } }
对这段代码,编译器会报使用了未经检查或不安全的操作。注: 有关详细信息, 请使用 -Xlint:unchecked 重新编译。
但是任然可以运行。
import java.util.*; public class C { public static void main(String[] args){ List<String> list2 = new ArrayList<String>(); list2.add("heheh"); list2.add("wuwuw"); //list2.add(35.5); for(int i = 0; i < list2.size();i++){ Object s = list2.get(i); System.out.println(s); } } }
而对于这段代码,添加了<String>类型参数以后,如果依然add(35.5)这个double类型的数,就会报错无法运行。因为加入的double类型与你已经确定的<String>类型冲突了,相当于你给add(String str)方法传入了一个double类型的实参。
对于接口来说,使用泛型的方式也是一样的。(List实际上就是一个接口的栗子)
接口使用泛型的一个例子是生成器,generator是工厂设计模式的一种应用,专门负责创建对象。一般而言,一个生成器只定义一个方法,该方法用以产生新对象。
public interface Generator<T>{
T next();//返回一个T类型的对象。
}
当你需要一个生成器时,就可以:
public class AGenerator implements Generator<A>{
public A next(){
......
}
}
当然,不仅仅类和接口可以实现泛型,方法也可以。以下是一个基本的指导原则:无论何时,只要你能做到,你就应该尽量使用泛型方法。要定义泛型方法,需将泛型参数置于返回值之前。与泛型类中的方法相比,区别在于只在该方法中出现了类型参数(或者在泛型类中,方法里出现了新的类型参数,需要把方法定义为泛型方法? 没有考证,想当然的个人理解)。
public class Lalala{
public <T> void f(T x){
System.out.println(x.getClass().getName());
}
}
注意,对于一个static的方法而言,无法访问泛型类的类型参数,所以,如果static方法需要使用泛型能力,就必须使其成为泛型方法。也就是说,如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。
class Lalala<T>{
static <T> void xixixi(T t){}
}
如果你希望有多个不同种类的参数的话,可以将<T>扩展成<A,B,C,D>这样的形式。
通配符:<? extends A>,这个类型参数可以代表A与所有继承了A的类型,而<? super A> (超类型通配符)则代表了所有A与A的超类作为类型参数。更特殊的还有无界通配符 <?>,表示: 我是想用Java的泛型来编写这段代码,我在这里并不要用原生类型,但是在当前这种情况下,泛型参数可以持有任何类型。 比如List(相当于List<object>)和List<?>,前者表示持有任何Object类型的原生List,后者表示 具有某种特定类型的非原生List,只是我们不知道那种类型是什么。
(在定义对象时)这并不是说通配符限制了这个参数的范围,恰恰相反,通配符反而让不同类之间可以交流:
Class A{}
Class B extends A{}
Class G<T>{}
//下面这行就会报错,尽管B的确是A的子类:不兼容的类型: C<B>无法转换为C<A>
//G<A> g = new G<B>();
但是如果改成这样
G<? extends A> g = new G<B>();
就不会报错。
而当在定义类或方法时使用了通配符,那就限制了传入参数的类型。
public class G<T extends A>{
private T key;
}
-
tip:如果你在定义类的时候使用了 class A<T extends E>,那你就真的限制了上界或者下界,注意与通配符区分。
尽管如此,以List作为G为例,你还是不能对g使用add()这样的方法(除了add(null)),因为编译器不知道g当时指向的会是哪一种具体的类型。你的g对象中可能有A的任何子类,由于向上转型,到底是什么子类没有人关心。
对于参数值是
未知类型
的容器类,只能
读取其中元素,不能
向其中添加元素, 因为,其类型是未知,所以编译器无法识别添加元素的类型和容器的类型是否兼容,唯一的例外是NULL
那么如何添加元素进去?只要那个函数接受的参数是Object就行了。对于上述的add例子,它的参数随着你创建g时变成了"? extends A",从这个描述中,编译器不能了解具体需要的是那个子类,所以不会接受任何类型的A。而对于接受参数是Object类型的函数,不涉及任何通配符,编译器就允许这个调用,add(Object o)这样就可以了。
对于<? super T>:与上述例子对于,它的get方法你不清楚返回的是哪个类型。
有一种情况特别需要使用<?>而不是原生类型。如果向一个使用<?>的方法传递原生类型,那么对编译器来说,可能会推断出实际的类型参数,使得这种方法可以回转并调用另一个使用这个确切类型的方法。这种技术成为捕获转换。捕获转换只有在这样的情况下可以工作:即在方法内部,你需要使用确切的类型。比如说f2()使用的是<?>,但是在f2()中调用了需要确切类型的函数f1(),这时候就会使用到捕获转换。
擦除特性:
在泛型代码内部,无法获得任何有关泛型参数类型的信息。
不管你的类型参数是什么,SomeClass<A>和SomeClass<B>使用getClass得到的是一样的。
如果你希望在SomeClass中调用A类的方法,会报错,因为编译器根本不知道你的类型参数A自己带有的方法。
二、容器
----事先注明,有些接口没有介绍,所以有些方法要自己看看相应的类实现了哪些接口才知道----
容器的用途是“保存对象”。包括Collection,Map两大类等。
类图关系大致 如图,但是实际上并不严谨,有些其他关系和抽象类什么的没有画出,在此仅为了表示一个直观的大概的印象,方便初学者理清层次关系:
(另,有画错的地方,Deque应该是Queue的子接口,并且LinkedList同时也继承并实现了它)
在介绍容器之前,先了解一下容器中存在的一个比较重要的成员:Iterator。
Iterator接口继承了顶级接口Iterable,通常用来遍历序列中的对象。
以Collection为例,它的用法是,当Collection的子类对象调用了iterator()方法,返回一个Iterator对象。该Iterator对象第一次调用next方法时,会获得(返回)Collection对象中的第一个元素。(可以把迭代器理解成一个指针,每次调用next方法,就指向下一个元素)
遍历时,通常用it.next()来表示遍历过程中的对象。
而在List中有一个listIterator方法,返回一个ListIterator对象,从类图中看,它可以双向遍历,也多了一些功能。要注意的是,add方法将指定元素插入当前位置之前。
(如果在遍历过程中,用了remove方法,是不影响遍历的,可以正常调用next等方法)。
除了ListIterator以外,还有可分割迭代器spliterator和倒序迭代器descendingIterator的存在。
一、Collection<T>
Collection<T>是一个泛型接口,List<T>,Set<T>是继承它的接口,再往下就分别是实现了List的LinkedList,ArrayList(以及Vector接口和实现了Vector的Stack),还有实现了Set<T>的TreeSet(实际上TreeSet的上层是SortedTree<T>,SortedTree<T>是Set<T>的子接口)和HashSet。
Object类中的equals(Object)方法用于检测一个对象是否等于另一个对象,在Object类中,这个方法判断的根据是两个对象是否具有相同的引用,如果是,那么就返回true。而hashCode方法(散列码)是由对象导出得到的一个整型值,不同的对象得到的散列码一般是不同的。由于hashCode方法定义在Object类中,因此每个对象都有一个默认的散列码,也就是对象的存储地址。如果需要覆写equals方法时,必须也覆写hashCode(也就是定义一个返回整型数的方法,这个整型数的产生方式要根据你的需要区分开不同的对象,比如调用该类中各个属性的hashCode方法然后做个运算)。equals和hashCode的定义必须一致:如果两个对象equals了,那么它们的散列码就必须具有相同的值。
tips:在字符串类型中,散列码是由内容导出的,所以内容相同散列码也相同。而StringBuffer类中没有定义hashCode方法,它的散列码就还是对象的存储地址。Double啊什么的类都覆写了自己的根据内容比较的方法。
两个toArray方法注意返回值,toArray()返回的是该Collectionl里所有元素对象的Object类型的数组(因此有时要注意转化类型),而toArray(<T>[])里这个参数就是用来存储这些元素的数组,返回的也正是这个数组。
范例:
String[] y = x.toArray(new String[0])
*@param a the array into which the elements of this collection are to be
* stored, if it is big enough; otherwise, a new array of the same
* runtime type is allocated for this purpose.
removeIf()方法移除的是符合参数格式的字符串。(有点python的列表解析时的感觉)
c.removeIf(obj -> obj%2 == 1)
最后强调一下,Collction接口中的方法,↓下面这些 都是有的,但是可能有些没有画在图里,也没有重复介绍了。
-
List<T>:特点是有序,元素有存入的前后之分,以线性方式存储。而且可以重复。
要注意一件事:正如前文所说,类图有很多没有画出来的其他类,比如一些中间的抽象类,这些对List等接口都是有影响的。如果你打开编辑器或IDE查看JDK中的源码,会发现接口中有部分父接口声明过的方法,在子接口中又重复声明了,why? 搜索了一下,没有得到一致准确的答案,但是大概可以确定的是,对菜鸡的我和还需要看这篇文章学习的朋友来说,应该是体验不到其中的差别的,只要照常调用方法就行了。所以这个坑先留着,有大神知道的话,希望一定要指导一下!
listIterator(int index)
方法是从index位置开始产生一个ListIterator对象,也就是第一次调用next()时返回的对象。subList(int fromIndex,int toIndex)
,左闭右开区间,返回一个新的List,并不修改原来的List。tip:
在遍历的过程中是不对List操作进行修改的,无论是删除和添加,因为如果在遍历中一直向集合中新增加元素,会造成死循环的,还有就是如果在遍历过程中删除元素,会造成数组下表越界等问题.
所以一种操作就是先找到要删的元素,放入一个List中,然后用removeAll方法。或者就直接用removeIf,亦或是用Iterator的remove方法在遍历过程中删除。
接下来重点介绍一下实现了List接口的两个类,
-
LinkedList(√):底层数据结构是链表。线程不安全。把这个当成数据结构课自己实现过的链表用就行了,当你需要用链表时,建立一个LinkedList对象,链表的一般操作都已经帮你实现了。
-
All Implemented Interfaces:
Serializable
,Cloneable
,Iterable<E>
,Collection<E>
,Deque<E>
,List<E>
,Queue<E>
看实现了的接口,大概就知道LinkedList可以用来实现链表、队列和双端队列了。
方法还是挺多的,但是学过数据结构,基本看名字就知道有什么用了,也没什么需要注意的。
clone
返回的是浅拷贝 @return a shallow copy of this {@code LinkedList} instance。peek
和element
的效果是基本一样的,返回第一个元素,只是如果没有元素时,peek返回null,element抛出一个异常。poll
和remove
一样,都是移除并返回第一个元素,但是为空时,前者返回null,后者抛异常。add
和offer
一样,都是把一个元素加到最后,为空的时候,使用add方法会报错,而offer方法会返回false。这两个方法分别来自List和Queue(大概也可以根据自己要用LinkedList干什么来选方法)pop
是加一个元素到front of the list,push就是返回并移除开头的元素。
-
-
ArrayList(√):底层数据结构是数组。线程不安全。粗略的把它当作一个能够长度不定,操作灵活的数组就行了。
-
All Implemented Interfaces:
Serializable
,Cloneable
,Iterable<E>
,Collection<E>
,List<E>
,RandomAccess
说白了就是把数组当成一个类,然后加入了一些对“数组”的方法操作。不过,很自然就能想到,便利的代价就是效率更低。
sort
方法就是传入一个比较器参数,把ArrayList里的参数按比较器来排序。clone
:@return a clone of this ArrayList instance,是浅拷贝。trimToSize
方法可以修剪ArrayList的容量,因为ArrayList会预留一些空间,用这个方法可以删除多余的。手动扩容:
ensureCapacity(int minCapacity)
。forEach(Consumer<? super E> action)
对每个元素执行action操作。
-
-
Vector:底层数据结构是数组。线程安全。Vector是List的子接口,和Queue是一个层次上的,而与实现Queue的Deque相对应的是,实现了Vector的Stack类。但是据说过时了,所以不介绍了,了解有这样一个东西就好。
-
-
Set:元素不可重复 。同时没有链表按序的特性。它最常被使用的是测试归属性(归属 性),可以很容易地询问某个对象是否在某个Set中,因此,查找就成了Set中最重要的操作。Set具有和Collection完全一样的接口,没有额外的功能。所以把Collection的类图,名字换成Set就行了。根据编程思想里的原话:
实际上Set就是Collection,只是行为不同。
(像LinkedList这些子接口会重复声明父接口的部分方法,大概也是一样的感觉?)
Set是基于对象的值来确定归属性的。
重点来看下面两种实现类。
-
TreeSet:基于TreeMap实现,其底层数据结构是红黑树(是一个自平衡的二叉树),保证元素的排序方式,默认是自然顺序(Comparable),也可以自定义比较器顺序(Comparator)。
-
All Implemented Interfaces:
Serializable
,Cloneable
,Iterable<E>
,Collection<E>
,NavigableSet<E>
,Set<E>
,SortedSet<E>
实现了NavigableSet接口意味着它支持一系列的查找、定位的操作。
TreeSet(NavigableMap\<E,Object> m)
构造方法通过传递的参数可以扩展对于边界查询的方法。TreeSet(Comparator<? super E> comparator)
传递的是一个Comparator参数,Comparator是一个接口,通过实现这个接口,可以传递一个自定义的排序的方式。元素将根据这个比较器进行排序。(一般是你传入的自定义类,实现了Comparator接口,从而达到给该类元素排序的目的,后面的PriorityQueue一样的道理)TreeSet(Collection<? extends E> c)
,创建了包含了Collection参数中所含有的元素的对象。TreeSet(SortedSet\<E> set)
,Constructs a new tree set containing the same elements and using the same ordering as the specified sorted set.和上面一个差不多但是连顺序也记录。public SortedSet\<E> subSet(E fromElement, E toElement)
调用了另一个subSet函数, return subSet(fromElement, true, toElement, false);headSet(E toElement)
得到一个以 toElement为上界(不包括)的Set(Returns a view of the portion of this set whose elements are strictly less than toElement.),而headSet(E toElement, boolean inclusive)
Returns a view of the portion of this set whose elements are less than (or equal to, if inclusive is true) toElement.,可以看出后面这个inclusive参数就是决定是否要包括toElement。tailSet类似,只不过是决定下界的。
subSet(E fromElement, boolean fromInclusive, E toElement, boolean toInclusive)
,则是返回处于上界下界之中的Set,两个Inclusive就不用解释了把。subSet(E fromElement, E toElement)
对于这个方法来说,默认是fromInclusive是true的,而toInclusive是false的,也就是,含头不含尾。由于TreeSet是有序的,所以就有first()方法返回the first (lowest) element,和它对应的是last()方法 。
而
floor
返回的是比floor
的参数更小或相等的元素中最大的那个(因为它是地板嘛)。注意和lower
的对比,lower
是不包含相等的。 也就是floor(E e)
返回的是 element <= e 中最大的,lower
则是 element < e。 与floor
对应的是ceiling
方法。对于
pollFirs
t和pollLast
,看到poll
就应该猜到了,(Retrieves and removes the first (lowest) /last(highest) element, or returnsnull
if this set is empty. ) 也就是说检索到以后还要把它取出来。TreeSet的clone方法返回的是:a shallow copy of this set 浅拷贝。
-
-
HashSet:底层数据结构是哈希表(是一个 元素 为链表 的 数组) 。和TreeSet不同,它的输出顺序是随机的。
-
All Implemented Interfaces:
Serializable
,Cloneable
,Iterable<E>
,Collection<E>
,Set<E>
HashSet存储的对象存取都是哈希值,自身先后通过hashcode()和equals()方法来判断元素是否重复。哈希值就是元素的身份证(map中的Key)。当hashCode值不相同,就直接存储了,当hashCode值相同时,会在判断一次euqals方法的返回值是否为true,如果为true则视为用一个元素,不用存储,如果为false,这些相同哈希值不同内容的元素都存放一个桶里(当哈希表中有一个桶结构,每一个桶都有一个哈希值)
在添加自定义对象的时候,两个类的属性值相等,但是依然会被判定为不同的元素,因为没有重写hashCode(),所以默认调用的是Object类的hashCode(),而不同类的hashCode一般是不同的。所以当你以自定义类对象为类型参数的时候,记得要重写。
构造方法:
HashSet构造的默认容量(initialCapacity)是16。
HashSet(int initialCapacity, float loadFactor)
,前一个是初始容量可以理解,后一个load factor,加载因子(表示元素填满的程度)。加载因子越高,空间开销越小,但是查找的成本就会提高;反之,空间开销大,查找的成本低。对于不用手动设置的三个初始化方式,它们的默认加载因子是0.75,是一个折中的值。(那个带dummy的也是没看懂)具体的解释找到这一段:
The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased. When the number of entries in the hash table exceeds the product of the load factor and the current capacity, the hash table is rehashed (that is, internal data structures are rebuilt) so that the hash table has approximately twice the number of buckets. As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the HashMap class, including get and put). The expected number of entries in the map and its load factor should be taken into account when setting its initial capacity, so as to minimize the number of rehash operations. If the initial capacity is greater than the maximum number of entries divided by the load factor, no rehash operations will ever occur.
还有这个(讲HashMap的一篇很全面的博客:https://www.cnblogs.com/skywang12345/p/3310835.html)
容量 是哈希表中桶的数量,初始容量 只是哈希表在创建时的容量。加载因子 是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。通常,默认加载因子是 0.75, 这是在时间和空间成本上寻求一种折衷。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 get 和 put 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。
-
-
3.Queue:队列的特征是先进先出,这部分功能在LinkedList的方法里实现了。(别忘了LinkedList也实现了DeQue接口,需要用到普通的队列功能的时候别搞错了)。
就这么几种方法。和LinkedList对比一下,就可以知道哪部分是实现Queue的,当用作一个队列的时候该用哪些方法。
而和Collection相比:provide additional insertion, extraction, and inspection operations.
-
Deque子接口
-
All Superinterfaces:
Collection<E>
,Iterable<E>
,Queue<E>
主要方法如下:
This interface extends the
Queue
interface. When a deque is used as a queue, FIFO (First-In-First-Out) behavior results. Elements are added at the end of the deque and removed from the beginning. The methods inherited from theQueue
interface are precisely equivalent toDeque
methods as indicated in the following table:Queue Method Equivalent Deque Method add(e)
addLast(e)
offer(e)
offerLast(e)
remove()
removeFirst()
poll()
pollFirst()
element()
getFirst()
peek()
peekFirst()
Deques can also be used as LIFO (Last-In-First-Out) stacks. This interface should be used in preference to the legacy
Stack
class. When a deque is used as a stack, elements are pushed and popped from the beginning of the deque. Stack methods are precisely equivalent toDeque
methods as indicated in the table below:Stack Method Equivalent Deque Method push(e)
addFirst(e)
pop()
removeFirst()
peek()
peekFirst()
从摘自API文档的这段介绍来看,当你需要FIFO结构时,意味着元素从队尾进,从队首出,就用继承自Queue接口的方法,而这些方法有了含义更清晰的表达,也就是表格中的对应关系,addLast把新来的元素放到队尾,效果和Queue的add一样的。而当你需要LIFO的栈结构, 下面那个也是一样的。
另外还两个方法:removeFirstOccurrence(Object o)和removeLastOccurrence(Object o),看方法名就知道是什么了。
-
-
PriortyQueue:优先队列。
-
All Implemented Interfaces:
Serializable
,Iterable
,Collection
,Queue
-
-
Constructor Description PriorityQueue()
Creates a PriorityQueue
with the default initial capacity (11) that orders its elements according to their natural ordering.PriorityQueue(int initialCapacity)
Creates a PriorityQueue
with the specified initial capacity that orders its elements according to their natural ordering.PriorityQueue(int initialCapacity, Comparator<? super E> comparator)
Creates a PriorityQueue
with the specified initial capacity that orders its elements according to the specified comparator.PriorityQueue(Collection<? extends E> c)
Creates a PriorityQueue
containing the elements in the specified collection.PriorityQueue(Comparator<? super E> comparator)
Creates a PriorityQueue
with the default initial capacity and whose elements are ordered according to the specified comparator.PriorityQueue(PriorityQueue<? extends E> c)
Creates a PriorityQueue
containing the elements in the specified priority queue.PriorityQueue(SortedSet<? extends E> c)
Creates a PriorityQueue
containing the elements in the specified sorted set.
可以看出和TreeSet有相似的地方,那就是可以传入Comparator参数。而这就是PriorityQueue的特点。Comparator是一个接口,根据Comparator中实现的规则,可以改变你入列以后的排序顺序。比如说你希望有一个按官职大小排队的队伍,在Comparator中实现了,传参进来,然后来了个县令,队里只有一个人,又来了个捕头,排第二没毛病(这个排序不是因为FIFO,而是因为他官职更小),这时候来了个尚书,因为他官职最大,所以他就排队到第一。出列的时候,没有变,队首出。这就是PriorityQueue。
其余的方法,看实现的接口就知道了,基本就是Queue的方法。
要说的话,public Comparator<? super E> comparator() { return comparator;} 这个方法可以返回该队列的Comparator属性(private final Comparator<? super E> comparator)。
二、Map
Map是和Collection一个层次上的接口,它的结构是key-value。
K,V两个参数分别对应了key和value,这样基本懂了大部分方法了。
entrySet、keySet、values
三个方法分别得到所有键值的Set(元素类型是Map的内部接口Entry,Entry提供了一些操作)、键的Set、值的Collection。
三个带compute的方法都是用来计算参数中的key对应的新value的。
getOrDefault
返回key对应的value,如果key不存在的,就返回defaultValue。
putIfAbsent
,如果不存在参数中的那个键值对或者值为空,就put。普通的put方法会直接覆盖。
remove(Object key, Object value)
移除的前提是二者都要与map中的键值对匹配,才能执行删除操作。
merge
,如果参数中的键不存在或者值为空,就用value和它组成一对;如果存在的话,就用第三个参数计算出来的结果代替它。具体怎么用,和compute差不多,写的时候还没有试过……
-
HashMap 散列表,存储键值映射的数据,不保证映射的顺序。 可以和TreeMap一起对比HashSet和TreeSet。
-
All Implemented Interfaces:
Serializable
,Cloneable
,Map
Constructor Description HashMap()
Constructs an empty HashMap
with the default initial capacity (16) and the default load factor (0.75).HashMap(int initialCapacity)
Constructs an empty HashMap
with the specified initial capacity and the default load factor (0.75).HashMap(int initialCapacity, float loadFactor)
Constructs an empty HashMap
with the specified initial capacity and load factor.HashMap(Map<? extends K,? extends V> m)
Constructs a new HashMap
with the same mappings as the specifiedMap
.又是和Hash挂钩的,所以又能看见load factor。
clone方法照例 Returns a shallow copy
没有其他特别的方法。
-
-
TreeMap: 底层实现基于红黑树。有序。
-
All Implemented Interfaces:
Serializable
,Cloneable
,Map<K,V>
,NavigableMap<K,V>
,SortedMap<K,V>
Constructor Description TreeMap()
Constructs a new, empty tree map, using the natural ordering of its keys. TreeMap(Comparator<? super K> comparator)
Constructs a new, empty tree map, ordered according to the given comparator. TreeMap(Map<? extends K,? extends V> m)
Constructs a new tree map containing the same mappings as the given map, ordered according to the natural ordering of its keys. TreeMap(SortedMap<K,? extends V> m)
Constructs a new tree map containing the same mappings and using the same ordering as the specified sorted map. 方法还挺多的,但是由于实现都基于红黑树,原理相同,所以基本可以和TreeSet对标。
-
-
LinkedHashMap:双向链表+HashMap
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
也就是说实现了HashMap的按存放顺序排列的功能。
附博客一篇:https://blog.csdn.net/justloveyou_/article/details/71713781
断断续续写那么多,也没什么规划,而且还有些理解上的错误没改过来,恐怕其他人很难读下去了……有坚持到这里的我表示深深的歉意。
写这篇博客最主要还是记录下自己学习的过程,一边捋思路,一边翻书,碰到难点了看博客,后面发现直接翻API更清楚,这个学习过程中得到的经验才是这篇博客最重要的收获,真正学到的,也就懒得再往回写了。
等继续深入容器的时候再来总结一下吧。