泛型,HashMap, Java 并发--- Java编程的逻辑

200 阅读11分钟
  • 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 B3.红黑树:
        
        
        
        
        
    
  • 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()