-
8.1.2 容器类
-
8.1.3 泛型方法
public static后边的<U,V>表示这个方法是一个泛型方法,并且持有两个泛型参数<U,V> Pair<U,V> 表示这个方法的返回值是Pair<U,V> public static <U,V> Pair<U,V> make_pair(U first, V second){ Pair pair = new Pair("move forward",100); return Pair; } 调用: Pair a = make_pair("move forward",100); -
8.1.4 泛型接口
通过给接口设置一个参数化类型,我们可以让接口的功能(既其中的方法)作用于不同的数据类型。 1.当一个具体类implements接口时,要声明接口中的参数类型,一般是声明为类本身。 比如Comparable接口: public interface Comparable<T>{ int compareTo(T o); } 当某个类实现了Comparable接口,如果我们将T设置为类本身,然后实现compareTo方法,那么这个类的对象就有了和自身比较的能力。 2.当这个类本身就是泛型类时,也要指明接口中的泛型参数,一般是和泛型类本身的参数保持一致。 因为接口本身是给类提供方法。在类中,我们实现这些方法,对某些数据类型进行操作。 类的泛型参数一般是用来参数化类要操作的对象: 比如HashMap<K,V>,k和v用来参数化entry,LinkedList<E>,E用来参数化Node 1.类一般有一个静态内部类,来提供一个功能单一的对象。泛型类的参数往往是提供给这个对象的。 2.接口提供参数化的空方法,接口提供这个参数化空方法给类,类对方法进行实现。 例如,List接口中有void add(int index, E element);这个E就是类中我们要操作的对象。 类中: void add(E e){ 具体实现 } 所以,接口的泛型参数必须和类的泛型参数保持一致。这样,接口才能和类配合,对小对象进行操作。 -
8.1.5 类型参数的限定
不相关的知识: 子类的构造器中必须调用父类的构造方法。我们可以在子类的构造方法中通过super指定调用父类的哪个构造方法。如果没有指定,系统会默认为子类构造器第一行添加一个super().这时,如果父类中没有无参构造方法,就会报错。 在父类中,如果没有构造方法,系统会默认提供一个无参构造方法。如果添加了有参构造方法,则系统不会添加默认的无参构造方法。 综上,可以看出,我们最好手动给父类写一个无参构造方法。 本节开始: ------------------------------------------------------------------ 1.上界为某个具体的类 我们可以将一个类的泛型参数上界设为某个具体类<U extends Number>. public class Point<U extends Number>{ U point; public Point(U p){ this.point = p; } public int doule_point_value(){ return 2*point.intValue(); } } 这样,参数化的point就可以使用Number的方法啦。 通过将参数的上界设置为某个类。在类中,这个参数化的对象可以使用上界类的方法。 并且,指定上界后,类型擦除时就不会转换为Object了,而是转换为上界类的对象。 2. 上界为某个接口。 public static <T extends Comparable<T>> T max(T[] arry){} 当上界为设定为接口时,T必须实现Comparable接口,Comparable接口的数据类型为T。 这样,T就可以和自身比较。 3. 上界为其他参数。 E是要被添加到的DynamicArray的参数类型,E为Number --------------------------------------------------- |Number1|Number2|Number3|Number4| | | | --------------------------------------------------- T是传入的DynamicArray的参数类型,他必须是E的子类,比如Integer ---------------------------- |Integer1|Integer2|Integer3| ---------------------------- public <T extends E> void addAll(DynamicArray<T> c){ for(int i=0;i<c.size;i++){ add(c.get(i)); //将c中的Integer添加到Number DynamicArray中 } } public void addAll(DynamicArray<? extends E> c){ for(int i=0;i<c.size;i++){ add(c.get(i)); //将c中的Integer添加到Number DynamicArray中 } } -
8.2解析通配符
8.2.2理解通配符 ?表示无限定通配符 无限定通配符?和类型参数T 有时候可以互相替换。 对于<? extends T>, ?表示T本身或者T的子类。这类通配符不能读取,不能写入。比如 ArrayList<? extends numbers> foo1 = new ArrayList<Integer>(); foo1.add(1); //会报错 foo1中不能存入任何元素,只能往出取。 ArrayList<? super Integer> foo2 = new ArrayList<Integer>(); foo2.add(1); //ok Object a = foo2.get(0); //取出来的东西只能放到Object里。 Integer it1 = foo2.get(0);//会报错 8.2.3超类型通配符 8.2.3 细节和局限性 -
HashMap
1. why we need to right shift key.hashcode() 16 bit? key.hashcode()的结果为什么要右移16位?---> 避免因hash值只在高位变化而产生的哈希冲突。 hashcode()的返还的结果是一个int,它的二进制表示是32位。计算最终hash值h_result时: h_result= (h = key.hashcode()) ^ h>>>16; 当我们计算在table的index时,我们使用: index= (n-1) & h_result; 那么,当table size n较小,比如说64. 且hashcode()返还的值只在高16位变化,而低16位没有变化。 key.hashcode()&(n-1) 总会给出相同的结果,会产生哈希冲突。 而当我们将: h_result= (h=key.hashcode()) ^ h>>>16,这个时候hashcode()的高16位会与低16位进行异或运算,也就是说,高16位的变化会传递到低16位。 而低16位通常决定着在table中的index: index = h_result & (n-1). 这样,就算hashcode()只在高位变化,那么,仍会产生不同的index。 2.为什么要重写key的hashCode()和equals()? 主要是为了解决两个具有相同内容的对象key的存取问题。当你用对象key a存,对象key b取,如果不重写hashCode()和equals(),就不会返回正确的键值对。 key a = new key("upup"); key b = new key("upup"); hashmap.put(a,123); hashmap.get(b); hash = key.hashcode() ^ (key.hashCode()>>>16) index = (n-1) & hash; 如果key是一个对象,如果不重写hashCode(), 那么两个具有相同内容的对象key返还的hash值是不同的,这两个对象会被哈希到hashmap数组中的不同index。 因为hashCode的默认方法是返还一个与内存地址相关的数。 当key是对象时,用两个内容相同的对象分别去存和取元素。先用A对象存,然后用B对象取。 当你用get方法时,传入一个B对象key。 hashmap会先通过 index=(n-1) & hash(B.key) 去找这个key对应的element在数组中的位置。然后找到这个index后,开始比较这个index里的elements的key和传入的key。 如果你不重写hashCode(),那么首先index的位置就是错的。因为,hash(A)和hash(B)的结果不一样。index of A可能是3, index of B可能是5. 然后,当开始比较index里elements的key和传入的key时 代码是: e代表hashmap数组中的element。 (e.hash == hash && (k=e.key)==key || ( key!=null && key.equals(k))) 对每一个element,还要先比较hash,然后比较key是否一致。 比较key一致:先用内存地址判断,再用equals()判断。 先比较key的内存地址,如果key的内存地址是相同的,我们就get到我们想要的对象。 如果内存地址不同,就是上边的情况,两个不同的对象key具有相同的内容,那么就要用key的equals方法去比较他们的内容。 所以,我们要重写equals()方法,对具有相同内容的两个对象key,返回true. 这样,我们就能正确的返回B对象对应的element了。 总结一句话: 重写hashCode()保证两个内容相同的对象key所对应的键值对能被哈希到同一个位置。 重写equals() 保证当我们比较key时,两个相同内容的对象key能被判定为true。 从而可以用对象key A存, 对象key B取 3.红黑树: -
Java并发
15.1 线程的基本概念 1) start()表示启动该线程,使其称为一条单独的执行流,每个线程会有单独的程序执行计数器和栈, 2) Thread有一个静态方法currentThread(), 返回当前执行的线程对象。 public static native Thread CurrentThread(), 通过它,我们可以在run方法中判断代码是在哪个线程执行的. currentThread()是Thread类的静态方法, 但是当我们用一个类继承了Thread类的时候,自然也就继承了CurrentThread()方法。用这个类生成的对象自然也就可以使用CurrentThread()方法,来表明当前是在在哪个线程中。 3)共享内存可见性,可以通过给变量加volatile解决。 15.2 理解synchronized a)synchronized化实例方法: 保护的是同一个对象内的synchronized实例方法们,确保同时只能有一个线程执行该对象的被synchronized的方法们。 “synchronized实例方法保护的是当前实例对象,既this. this对象有一个锁和一个等待队列,锁只能被一个线程持有,其他试图获得同样的锁的线程需要等待。 执行synchronized实例方法的过程大致如下: 1) 线程尝试获得锁,如果获得锁,进入对象内部,执行代码; 如果不能获得,则加入等待队列,阻塞并等待被唤醒。 2) 执行实例方法体代码。 3) 释放锁;从this对象的等待队列中随机唤醒一个线程,并给予锁,唤醒哪一个是不一定的,不保证公平性。 只有调用对象内部synchronized方法的线程会被阻塞,获得锁,进入对象内部,执行synchronized方法。 如果一个线程调用的是非synchronized方法,则它并不会被阻塞。 所以,对于会被多个线程修改的变量,我们要在修改它的所有成员方法上加上synchronized方法。 b)静态方法 调用synchronized静态方法的线程,会获得类对象锁。 获得类对象锁之后,该线程就可以访问所有被被synchronized的静态方法。 需要注意,类对象锁和实例对象锁是两个不同的锁。 c)代码块 和上边类似,只不过把synchronized关键字放在了方法中 public void incr(){ synchronized(锁){ ...具体代码 } } 这个锁可以是实例对象锁,也可以其他锁,你可以在类中声明一个锁。比如: private Object lock = new Object(); public void incr(){ synchronized(lock){ ...具体代码 } } 虽然加了synchronized后,所有方法调用变成了原子操作,但这并不意味着是绝对安全的。以下情况需要注意: 1. 复合操作 如果对一个对象的A操作分为两步A.a(), A.b(),每步分别获得锁。 那么,当多个线程对对象A进行操作时,我们希望如果有一个线程执行了A.b(),其他线程就不要执行A.b()了。 但是,因为我们对A.a(),A.b()分别加锁。 如果有线程1执行了A.a(),返还锁。 当线程1没有执行A.b()之前,线程2会获得锁,并再次执行A.a()。 之后,A.b()就会被线程1和线程2分别调用。这不是我们所希望的。 2.伪同步 如果我们对执行了A.a(), A.b()这两步的方法加锁呢? 还是不行,我们必须要使用A对象锁来对A.a(), A.b()加锁。 public void method(){ synchronized(A){ A.a(); A.b(); } } public void method1(){ A.a(); A.b(); } 这样,如果多个线程访问对象A,并调用A.a()和A.b(). 因为对象A作为锁 被 method()方法的synchronized获取了,其他访问对象A的非synchronized方法将不能调用A.a(),必须等待正在执行method()的线程执行完毕,并返还A锁。 这样,其他非synchronized实例方法中对A对象方法的调用才能执行。 3.迭代 如果我们在迭代一个容器的时候,这个容器被添加了元素,就会抛出异常。 因此,在迭代的线程中, 我们要用synchronized关键字将被迭代的容器锁住。 public void run(){ while(true){ synchronized(list){//这里,锁住list,就不会抛出异常了。 for(String str: list){ } } } } 4.并发容器 同步容器的性能较低,当并发容器访问量比较大时,性能较差,所幸的是,Java中还有很多专门为并发设计的容器类。 CopyOnWriteArraylist ConcurrentHashMap ConcurrentLinkedQueue ConcurrentSkipListSet 这些容器都是线程安全的,都没有使用synchronized, 支持复合操作,没有迭代问题,性能也高的多。 15.3 线程的基本协作机制 wait()/notify(): 围绕一个共享变量进行协作。 public class WaitThread{ public void method1(){}; public void method2(){}; } wait()/notify()方法必须放在synchronized代码块中调用。 public void method1(){ synchronized(this){ while(!fire){ wait(); } ... } } 调用wait()后,线程会被添加到条件队列,并释放对象锁,状态变为WAITING或者TIMED_WAITING. public void method2(){ synchronized(this){ fire=true; notify(); ... } } 调用method2的线程获得对象锁,执行synchronized代码块,将共享变量设为true,并执行notify()方法。 notify()会随机唤醒条件队列上一个等待的线程。但是,notify()并不会释放对象锁,等到method2()里的synchronied代码块里的代码执行完后,释放锁。 method1()里的线程被唤醒后,要重新竞争对象锁,如果获得锁,线程状态变为RUNNABLE, 线程从wait()调用中返回。如果没有竞争到锁,线程加入对象锁等待队列,线程状态变为BLOCKED.只有获得对象锁之后,线程才能从wait()中返还。 获得锁,并从wait()中返回后,线程会再次检查while条件,如果成立,会继续调用wait()。如果不成立,会向下执行。 15.3.3 生产者消费者模型 学习Queue 和 Deque; 1).Queue inteface的方法: Queue<Integer> q = new ArrayDeque<Integer>(); add(), offer() : 加到队尾 poll(),remove():弹出队列首位 peek(),element():查看队列首位 2).Dequeue interface有用的方法: Dequeue<Integer> q = new ArrayDeque<Integer>(); addFirst(),addLast() removeFirst(), removeLast() 3)学习一下Collections的sort方法: sort()对list进行升序排序,无论是Integer还是String. Collections.sort(list); -------------------------------------------- list进行降序排序:重写Comparator Collections.sort(list,new Comparator<Integer>(){ @Override public int compare(Integer o1, Integer o2){ return o2-o1; } }); list进行降序排序:Collections.reverseOrder() Collections.sort(list, Collections.reverseOrder()); --------------------------------------------- 对array进行升序排序: Arrays.sort(list) 对array进行降序排序: Arrays.sort(list,Collection.reverseOrder()); 其实Collections.sort()最后调用的也是Arrays.sort()