集合学习
一、通用与特定方法
1.遍历
1)迭代器遍历
迭代器本质是一种设计模式,为了解决不同的集合类提供统一的遍历操作接口。
迭代器结构
public interface Iterator<E> {
boolean hasNext();
E next();
default void remove() {
throw new UnsupportedOperationException("remove");
}
//对剩余集合剩余元素执行操作
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}
运用
public void testIteratorAndForEachRemaining() {
int count = 0;
Collection<String> coll = new ArrayList<String>();
coll.add("abc1");
coll.add("abc2");
coll.add("abc3");
coll.add("abc4");
Iterator<String> it = coll.iterator();
//while截断在第二个位置
while (it.hasNext()) {
if (count > 1) {
break;
}
System.out.println(it.next());
count++;
}
//使用迭代器的forEachRemaining方法传入有个拼接好的事件:从第三个位置之后先一遍大写后一遍小写输出
it.forEachRemaining(consumerString(s -> System.out.print(s.toUpperCase()), s -> System.out.println(s.toLowerCase())
));
}
//拼接两个Consumer
private static Consumer consumerString(Consumer<String> one, Consumer<String> two) {
return one.andThen(two);
}
//消费指定泛型
private static void consumerString(Consumer<String> function) {
function.accept("Hello");
}
2)Map遍历
table 与 entrySet 与keySet 与 values
1.transient Node<K,V>[] table 与 transient Set< Map.Entry<K,V>> entrySet
*0.transient关键字:短暂的,即序列化的时候不会被序列化
1.为了使得遍历方便(Map.Entry接口中有getKey和getValue方法)会将table中对应的Node(一个Node结点的内容见上)存入一个Set集合,达到维护效果。
2.为什么Node类型数据可以存入Map.Entry是因为Node本身就实现了Map.Entry
3.怎么存入的,设定指针指向
发生时机:调用entrySet时,源码如下:
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
会创建一个final修饰的EntrySet对象,当调用该对象的ForEach时装填table里的数据:
public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
2.Collection vs = values;和Set ks = keySet;
1.本质和上面一致就是获取table中单独的key集合和values集合
log.info("1.通过entrySet");
map.entrySet().forEach(System.out::println);
log.info("简略版:底层一致");
map.forEach((k, v) -> System.out.println(k + " : " + v));
log.info("2.通过keySet");
Set<String> strings = map.keySet();
for (String string : strings) {
System.out.println(map.get(string));
}
log.info("3.通过Values");
map.values().forEach(System.out::println);
2.TreeSet(TreeMap)与比较器
TreeMap 它还实现了NavigableMap 接⼝和 SortedMap 接⼝。
//TreeSet构造函数实现了一个比较器对象,底层就是把这个比较器丢给TreeMap
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class Person implements Comparable {
Integer age;
String name;
@Override
public int compareTo(@NotNull Object o) {
if (o instanceof Person) {
Person p = (Person) o;
//返回1则交换位置:0与-1比较大于返回1则说明要交换即this.name<p.name说明要交换
return Integer.compare(0,this.name.compareTo(p.name));
}
return 0;
}
}
1.TreeMap是用比较器排序的Map,其他Map都不行,它可以支持两种比较器实现排序。当所有比较结果都相同时前者就会被后来的替换,如下:
Map<Person, String> treeMap = new TreeMap<>();
treeMap.put(new Person(1, "a"), "person1");
treeMap.put(new Person(3, "aa"), "person2");
treeMap.put(new Person(2, "aa"), "person3");
treeMap.put(new Person(4, "aaaa"), "person4");
treeMap.forEach((key, value) -> System.out.println(key+" "+value));
结果:
Person(age=4, name=aaaa) person4
Person(age=3, name=aa) person3//person2和person3的名字一致被替换
Person(age=1, name=a) person1
2.但条件可以满足互斥时会保留,用一个Comparator重新排序就会在原来基础上再排序一次:
return Integer.compare(0,this.name.compareTo(p.name));
===========================================
Map<Person, String> treeMap = new TreeMap<>(Comparator.comparingInt(Person::getAge).reversed());
treeMap.put(new Person(1, "a"), "person1");
treeMap.put(new Person(3, "aa"), "person2");
treeMap.put(new Person(2, "aa"), "person3");
treeMap.put(new Person(4, "aaaa"), "person4");
treeMap.forEach((key, value) -> System.out.println(key+" "+value));
结果:
Person(age=4, name=aaaa) person4
Person(age=3, name=aa) person2
Person(age=2, name=aa) person3
Person(age=1, name=a) person1
*2.1Comparator与Camparable使用
一个类实现了Camparable接口则表明这个类的对象之间是可以相互比较的,这个类对象组成的集合就可以直接使用sort方法排序。
Comparator可以看成一种算法的实现,将算法和数据分离,Comparator也可以在下面两种环境下使用:
1、类的设计师没有考虑到比较问题而没有实现Comparable,可以通过Comparator来实现排序而不必改变对象本身
2、可以使用多种排序标准,比如升序、降序等。
总结一下,两种比较器Comparable和Comparator,后者相比前者有如下优点:
1、如果实现类没有实现Comparable接口,又想对两个类进行比较(或者实现类实现了Comparable接口,但是对compareTo方法内的比较算法不满意),那么可以实现Comparator接口,自定义一个比较器,写比较算法。
2、实现Comparable接口的方式比实现Comparator接口的耦合性要强一些,如果要修改比较算法,则需要修改Comparable接口的实现类,而实现Comparator的类是在外部进行比较的,不需要对实现类有任何修改。从这个角度说,实现Comparable接口的方式其实有些不太好,尤其在我们将实现类的.class文件打成一个.jar文件提供给开发者使用的时候。实际上实现Comparator 接口的方式后面会写到就是一种典型的策略模式。
对于本身实现了Comparator接口的方法(一般集合都有)就直接内部类或者Lambda表达式写排序逻辑即可。
//对于List或者Array需要调用对应的sort之后在类中重写的compareTo才会生效
List<Person> people = new ArrayList<>();
people.add(new Person(3, "aaaaa"));
people.add(new Person(2, "aa"));
people.add(new Person(1, "aaa"));
people.add(new Person(4, "aab"));
Collections.sort(people);
结果:
Person(age=4, name=aab)
Person(age=3, name=aaaaa)
Person(age=1, name=aaa)
Person(age=2, name=aa)
//也可以实现Comparator覆盖
List<Person> people = new ArrayList<>();
people.add(new Person(3, "aaaaa"));
people.add(new Person(2, "aa"));
people.add(new Person(1, "aaa"));
people.add(new Person(4, "aab"));
people.sort((o1, o2) -> Integer.compare(0, o1.age.compareTo(o2.age)));
结果:
Person(age=4, name=aab)
Person(age=3, name=aaaaa)
Person(age=2, name=aa)
Person(age=1, name=aaa)
List<Person> personList = new ArrayList<>();
personList.add(new Person("Sherry", 9000, 24, "female", "New York"));
personList.add(new Person("Tom", 8900, 22, "male", "Washington"));
personList.add(new Person("Jack", 9000, 25, "male", "Washington"));
personList.add(new Person("Lily", 8800, 26, "male", "New York"));
personList.add(new Person("Alisa", 9000, 26, "female", "New York"));
// 按工资升序排序(自然排序)
List<String> newList = personList.stream().sorted(Comparator.comparing(Person::getSalary)).map(Person::getName)
.collect(Collectors.toList());
// 按工资倒序排序
List<String> newList2 = personList.stream().sorted(Comparator.comparing(Person::getSalary).reversed())
.map(Person::getName).collect(Collectors.toList());
// 先按工资再按年龄升序排序
List<String> newList3 = personList.stream()
.sorted(Comparator.comparing(Person::getSalary).thenComparing(Person::getAge)).map(Person::getName)
.collect(Collectors.toList());
// 先按工资再按年龄自定义排序(降序)
List<String> newList4 = personList.stream().sorted((p1, p2) -> {
if (p1.getSalary() == p2.getSalary()) {
return p2.getAge() - p1.getAge();
} else {
return p2.getSalary() - p1.getSalary();
}
}).map(Person::getName).collect(Collectors.toList());
System.out.println("按工资升序排序:" + newList);
System.out.println("按工资降序排序:" + newList2);
System.out.println("先按工资再按年龄升序排序:" + newList3);
System.out.println("先按工资再按年龄自定义降序排序:" + newList4);
3.fail-fast和fail-safe
快速失败(fail-fast) 是 Java 集合的⼀种错误检测机制。在使⽤迭代器对集合进⾏遍历的时候,我们在多线程下操作⾮安全失败(fail-safe)的集合类可能就会触发 fail-fast 机制,导致抛出
ConcurrentModificationException ConcurrentModificationException 异常。 另外,在单线程下,如果在遍历过程中对集合对象的内容进⾏了修改的话也会触发 fail-fast 机制。
举个例⼦:多线程下,如果线程 1 正在对集合进⾏遍历,此时线程 2 对集合进⾏修改(增加、删除、修改),或者线程 1 在遍历过程中对集合进⾏修改,都会导致线程 1 抛出ConcurrentModificationException 异常。
为什么呢?
每当迭代器使⽤ hashNext() / next() 遍历下⼀个元素之前,都会检测 modCount 变量是否为等于expectedModCount 值,是的话就返回遍历;否则抛出异常,终⽌遍历。
如果我们在集合被遍历期间对其进⾏修改的话,就会改变 modCount 的值,进⽽导致 modCount !=expectedModCount ,进⽽抛出 ConcurrentModificationException 异常。
Iterator 源码:
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
每次调用next()方法,在实际访问元素前,都会调用checkForComodification方法,该方法源码如下:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
单线程错误:
list.add("a"); list.add("b");list.add("c");
for (String s : list) {
list.remove("a");
}
结果:
java.util.ConcurrentModificationException
多线程错误:
List<String>list=getList();
//多线程操作失败
new Thread(()->{
for (String str :list) {
System.out.println(str+" thead1");
list.remove(str);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"a").start();
new Thread(()->{
for (String str :list) {
System.out.println(str+" thead2");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"b").start();
//===============================结果====================
a thead1
bb thead2
Exception in thread "a" java.util.ConcurrentModificationException
ccc thead2
d thead2
ee thead2
fff thead2
g thead2
h thead2
i thead2
解决方法一:在单线程的遍历过程中,如果要进行remove操作,可以调用迭代器的remove方法而不是集合类的remove方法。看看ArrayList中迭代器的remove方法的源码:
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
可以看到,该remove方法并不会修改modCount的值,并且不会对后面的遍历造成影响,因为该方法remove不能指定元素,只能remove当前遍历过的那个元素,所以调用该方法并不会发生fail-fast现象。
阿⾥巴巴⼿册相关的规定:不要在foreach循环里进行元素的remove/add操作,remove元素使用Iterator,如果并发需要对Iterator上锁。
迭代器方法删除:
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
if("a".equals(iterator)){
iterator.remove();
}
iterator.next();
}
结果:
[b, c]
另外removeIf方法满足迭代安全条件,源码如下:
default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}
测试:
list.add("a");
list.add("b");
list.removeIf(s->s.equals("a"));
log.info(list.toString());
结果:
[b]
有了前⾯讲的基础,我们应该知道:使⽤ Iterator 提供的 remove ⽅法,可以修改到
expectedModCount 的值。所以,才不会再抛出 ConcurrentModificationException 异常。
什么是安全失败(fail-safe)呢?
明⽩了快速失败(fail-fast)之后,安全失败(fail-safe)我们就很好理解了。
采⽤安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,⽽是先复制原有集合内容,在拷⻉的集合上进⾏遍历。所以,在遍历过程中对原集合所作的修改并不能被迭代器检测到,故不会抛ConcurrentModificationException 异常。在JUC包下可以采用满足fail-fast的容器CopyOnWriteArrayList,ConcurrentHashMap:
Fail Fast Iterator | Fail Safe Iterator | |
---|---|---|
Throw ConcurrentModification Exception | Yes | No |
Clone object | No | Yes |
Memory Overhead | No | Yes |
Examples | HashMap,Vector,ArrayList,HashSet | CopyOnWriteArrayList,ConcurrentHashMap |
二、扩容机制与初始化
1.ArrayList<?>(数组)
1.初始化
ArrayList<?>底层维护一个transient Object[] elementData ,数组初次创建未设定ArrayList的initialCapacity参数时容量为0,第一次添加扩容为10,再次扩容(第一个对象)时为elementData的1.5倍。初始设定为指定大小则也变为1.5倍。
//初始化 Collection<String> col = new ArrayList<>(2);
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
//没有initialCapacity就长度扩容为10
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
//扩容
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
2.扩容
迭代扩容之前执行ensureCapacityInternal(size + 1)进行扩容,当elementData的大小不够则调用grow(int minCapacity):minCapacity(当前元素+1后的长度)
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//1+0.5=1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//最大为Intager最大值为(2^31)-1
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);
}
2.Vecotor扩容(数组)(线程安全)
1.初始化
基础方法使用了synchronized关键字可以实现线程安全,默认是10,扩容为两倍,初始化时候可以设定初始大小和扩容数量
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
2.扩容
扩容机制与ArrayList差不多
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//capacityIncrement为初始设定的增长量,没有设置默认为0
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
3.LinkedList(双向链表)
1.初始化
用一个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;
}
}
2.扩容
add方法为尾插法
/**
* Links e as last element.
*/
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
//用来记录列表被操作次数
modCount++;
}
扩容:不用说了吧
增删多用LinkedList链表查询的时候需要从头开始检索,改查多用ArrayList数组增删需要改变剩余所有索引
4.HashSet(HashMap)
初始大小+负载因子
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
第一次添加默认为16的Node数组,临界值为数组长度*负载因子(0.75)到达临界值扩容到(原长度两倍 * 负载因子):16:12->32:24->64:48…
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
//定义辅助变量
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
//计算扩容大小,并初始化扩容(16个null的tab数组)
n = (tab = resize()).length;
//hash计算该key需要存的数组位置:此处的hash算法默认为
//hash=(h = key.hashCode()) ^ (h >>> 16)
//用key的hashCode高低位相亦或使得数字更随机(更散列)减少冲突
//因此当key为String且相等时候必会hash冲突
//其他情况要产生冲突就要满足hash值相等很难的吧
//首先判断这个位置上是不是为null(没存过东西)
if ((p = tab[i = (n - 1) & hash]) == null)
//为没存过的位置直接放入结点
tab[i] = newNode(hash, key, value, null);
else {
//该hash位置上有了就是hash了就得分以下三种情况
//先定义一些辅助变量
Node<K,V> e; K k;
//情况一:原位置上的结点p与新加入结点的hash和key(用==和equals都进行一次比较,满足其中一个就行)都相等则放弃。
//(可重写手动制造冲突:重写hashCode方法使hash值一样equals不一样就可以链化树化)
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//情况二:原位置上的p是一棵红黑树:在树上添加当前结点并赋值一份给e结点(添加成功则返回null;失败(树上已经有了)则返回结点对象)
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//情况三:是个链表就比较和其中的每个结点比较
//1.都不相同则挂在最后,e==p.next==null
//再判断是否大于等于8个结点要树化了或者扩容
//(所以默认16初始大小的链表的极限就是7+3=10:扩容三次:每次扩容会重写计算hash)
//treeifyBin源码代码:
//if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY(64))
// resize();
//2.遇到地址值和hash值都相等的或者equals的就break将此处的结点已赋值给e
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//e不为空就是已经存在了就别加进来了
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
//此处替换value:
//hashmap来说就是后来value替换前面的,hashset的话没差
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//每加入一个结点:链表或者数组都算 size会++;threshold数组实际长度
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
5.HashTable(HashMap)
底层是一个Entry数组,不允许键值为null,线程安全(内部方法大部分有synchronized修饰)
rehash():初始化为11:8之后扩容也是负载因子0.75,key相同value替换。
int newCapacity = (oldCapacity << 1) + 1//乘以二+1
//骚一点的遍历
treeMap.entrySet().stream().forEach(personStringEntry -> {
System.out.println(personStringEntry.getValue());
});
*5.1Properties类
继承自HashTable,可以读取Properties类文件
//方法二
private static void readProperty2() {
Properties properties = new Properties();
try {
properties = PropertiesLoaderUtils.loadAllProperties("code.properties");
//遍历取值
Set<Object> objects = properties.keySet();
for (Object object : objects) {
System.out.println(new String(properties.getProperty((String) object).getBytes("iso-8859-1"), "gbk"));
}
} catch (IOException e) {
e.printStackTrace();
}
}
*5.2properties配置文件与yml配置文件
0.参考文献
1.区别:基本上格式上的区别罢了。
2.一些基本的配置springboot会自动的读取,比如:datasource的配置会自动装配连接池
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useAffectedRows=true&serverTimezone=Asia/Shanghai&allowMultiQueries=true
username: root
password: root
3.自定义的读取
1.依赖
<!--spring默认使用yml中的配置,但有时候要用传统的xml或properties配置,就需要使用spring-boot-configuration-processor-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
2.配置:
ly:
upload:
baseUrl: http://image.leyou.com/
allowTypes:
- image/jpeg
- image/jpg
- image/png
3.读取:
类上
@Data
@ConfigurationProperties(prefix = "ly.upload")
public class UploadProperties {
private String baseUrl;
private List<String> allowTypes;
}
方法上
@Configuration //相当于配置文件,在本项目中使用@Autowired注入在此用@Bean注入的类时会启用这里的配置
public class WXPayConfiguration {
//注入配置类,在方法上读取配置文件信息完成注入
@Bean
@ConfigurationProperties(prefix = "ly.pay")
public PayConfig payConfig(){
return new PayConfig();
}
//注入官方提供的支付类
@Bean
public WXPay wxPay(PayConfig payConfig){
return new WXPay(payConfig, WXPayConstants.SignType.HMACSHA256);
}
}
4.使用:
4.1.在配置类 @Bean上的@ConfigurationProperties可以直接用@Autowired注入
4.2.在类上的@ConfigurationProperties使用配置类需
@EnableConfigurationProperties(UploadProperties.class)
三、泛型
1.作用和意义
类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),
然后在使用/调用时传入具体的类型(类型实参)。这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
泛型类
@Data
//在实例化泛型类时,可以指定T的具体类型,也可以不指定
class Generic<T> {
private int age;
private String name;
private T key;
public void Generic(T key) {
this.key = key;
}
}
指定后就限定了此对象的T的类型,没指定就可以是任意类型
注意:
- 泛型的类型参数只能是类类型,不能是简单类型。
- 不能对确切的泛型类型使用instanceof操作。如下面的操作是非法的,编译时会出错。
if(ex_num instanceof Generic){ }
泛型接口
/**
* 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
* 即:class FruitGenerator<T> implements Generator<T>{
* 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class"
*/
class FruitGenerator<T> implements Generator<T>{
@Override
public T next() {
return null;
}
}
2.通配符
当实现泛型接口的类,传入泛型实参时:
/**
* 传入泛型实参时:
* 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
* 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
* 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
* 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。
*/
public class FruitGenerator implements Generator<String> {
private String[] fruits = new String[]{"Apple", "Banana", "Pear"};
@Override
public String next() {
Random rand = new Random();
return fruits[rand.nextInt(3)];
}
}
同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。在逻辑上Generic不能视为Generic的父类。
public void showKeyValue1(Generic<Number> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}
Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);
showKeyValue(gNumber);
// showKeyValue这个方法编译器会为我们报错:Generic<java.lang.Integer>
// cannot be applied to Generic<java.lang.Number>
// showKeyValue(gInteger);
解决:使用通配符“?”
public class GenericTest {
public static void main(String[] args) {
Box<String> name = new Box<String>("corn");
Box<Integer> age = new Box<Integer>(712);
Box<Number> number = new Box<Number>(314);
getData(name);
getData(age);
getData(number);
}
public static void getData(Box<?> data) {
System.out.println("data :" + data.getData());
}
}
3.类型通配符上限和类型通配符下限
类型通配符上限通过形如<? extends Number>形式定义(必须为Number的子类),相对应的,类型通配符下限为<? super Number>形式(必须是Number的父类),其含义与类型通配符上限正好相反
Box<String> name = new Box<String>("corn");
Box<Integer> age = new Box<Integer>(712);
Box<Number> number = new Box<Number>(314);
getData(name);
getData(age);
getData(number);
//在代码//1处调用将出现错误提示,而//2 //3处调用正常。
//getUpperNumberData(name); // 1
getUpperNumberData(age); // 2
getUpperNumberData(number); // 3
四、结构
1.ConcurrentHashMap
jdk1.7
put
首先是通过 key 定位到 Segment,之后在对应的 Segment 中进行具体的 put。
Segment<K,V> s;
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
s = ensureSegment(j);
return s.put(key, hash, value, false);
虽然 HashEntry 中的 value 是用 volatile 关键词修饰的,但是并不能保证并发的原子性,所以 put 操作时仍然需要加锁处理。
HashEntry<K,V> node =
tryLock() ? null :scanAndLockForPut(key, hash, value);
finally {
unlock();
}
get
只需要将 Key 通过 Hash 之后定位到具体的 Segment ,再通过一次 Hash 定位到具体的元素上。由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。ConcurrentHashMap 的 get 方法是非常高效的,因为整个过程都不需要加锁。
size
在 JDK1.7 中,第一种方案他会使用不加锁的模式去尝试多次计算 ConcurrentHashMap 的 size,最多三次,比较前后两次计算的结果,结果一致就认为当前没有元素加入,计算的结果是准确的。 第二种方案是如果第一种方案不符合,他就会给每个 Segment 加上锁,然后计算 ConcurrentHashMap 的 size 返回。
jdk1.8
抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized
来保证并发安全性。
1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(O(logn)
),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。
put
- 根据 key 计算出 hashcode 。
- 判断是否需要进行初始化。
- 为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
- 如果当前位置的
hashcode == MOVED == -1
,则需要进行扩容。 - 如果都不满足,则利用 synchronized 锁写入数据。
- 如果数量大于
TREEIFY_THRESHOLD
则要转换为红黑树。
get
- 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
- 如果是红黑树那就按照树的方式获取值。
- 就不满足那就按照链表的方式遍历获取值。
size
Map 的 size 可能超过 MAX_VALUE, 所以还有一个方法 mappingCount(),JDK 的建议使用 mappingCount() 而不是size()。无论是 size() 还是 mappingCount(), 计算大小的核心方法都是 sumCount()。
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
ConcurrentHashMap 提供了Long baseCount、CounterCell [] counterCells 两个辅助变量和一个 CounterCell 辅助内部类。sumCount() 就是beseCount的值+迭代 counterCells 来统计 sum 的过程。
put 操作时,肯定会影响 size(),在 put() 方法最后会调用 addCount() 方法。
addCount() 代码如下:如果 counterCells == null, 则对 baseCount 做 CAS 自增操作。
如果并发导致 baseCount CAS 失败了使用 counterCells。如果counterCells CAS 失败了,在 fullAddCount 方法中,会继续死循环操作,直到成功。