Head First 十二 十三

第十二章 网络和线程

客户机方面

从Socket读数据:

这时候java 流的分层设计就体现出优势了。典型步骤为:
1、Socket chatSocket = new Socket("127.0.0.1", 5000);
建立Socket,127.0.0.1是表示localhost,即是本机。5000是端口。

2、InputStreamReader stream = new 
                              InputStreamReader(chatSocket.getInputStream());
InputStreamReader是byte流,和character流之间的一个桥梁。这也是个分层设计的体现,上层的byte流直接去读socket内的数据,不用关心底层socket怎么回事。直接请求socket给一个Input流。

3、BufferedReader reader = new BufferedReader(stream);
     String message = reader.readLine();
再次使用BufferedReader,chain stream 去连接connection stream。逐步分层。


向Socket写数据:

1、Socket chatSocket = new Socket("127.0.0.1", 5000);

2、PrintWriter writer = new PrintWriter(chatSocket.getOutputStream());


3、writer.println("message to send");
     writer.print("another message");


服务器方面

1、ServerSocket serverSock = new ServerSocket(4242);
在一个端口上建立ServerSocket,表示监听此端口上的连接请求。

2、Socket sock = new Socket("190.154.1.122", 4242);
这是客户机上的代码,用于请求与一个服务器建立连接。190那个IP是服务器的。

3、Socket sock = serverSock.accept();
这句话是在服务器上的代码,写出来后,服务器就开始一直等待,处于监听并接收请求的状态。然后一旦有客户机的请求进来,服务器立即响应,再新建立一个Socket,接收客户机的socket信息(即上面的Socket sock语句),这样serverSock就能再次处在监听状态,等待下一个请求。

注意:监听过程一般套在while (true)语句里,以让监听持续进行。

代码示例

这是个 服务器代码,就是一旦有请求进来,就给它返回一个 Advise。

import java.io.*;
import java.net.*;      别忘了引包

public class DailyAdviceServer {
     String[] adviceList = {"Go to room", "Eat the cat", "Sleep in bed", "Catch the dog"};

     public void go() {
          try {
          ServerSocket serverSock = new ServerSocket(4242);

          while (true) {
               Socket sock = serverSock.accept();

               PrintWriter writer = new PrintWriter(sock.getOutputStream());
               String advice = getAdvice();
               writer.println(advice);
               writer.close();

               System.out.println(advice);
               
          }          
     } catch (IOException ex) {
          ex.printStackTrace();
     }
     }

     private String getAdvice() {
          int index = (int)(Math.random() * adviceList.length());
          return adviceList[index];
     }

     public static void main(String[] args) {
          DailyAdviceServer server = new DailyAdviceServer();
          server.go();
     }
}
注意:上面这个服务器代码,执行的时候一次只能响应一个请求,如果同时还有请求,就无法响应,解决办法是线程。

客户端代码,功能就是点击send后,一条消息发送到上面写的那个Server上,要与server建立连接的,server必须首先存在才行,这个客户端只能自娱自乐,收不到服务器信息:

import  java.awt.*;
import  java.awt.event.*;
import  java.io.*;
import  java.net.*;

import  javax.swing.*;


public  class  MyGUI {
        private  JTextField  textSending ;
        private  PrintWriter  writer ;
        public  void  go(){
              JFrame frame =  new  JFrame();
              frame.setDefaultCloseOperation(JFrame. EXIT_ON_CLOSE );
              
              JPanel panel =  new  JPanel();
              
               textSending  =  new  JTextField(20);
              JButton sendButton =  new  JButton( "Send" );
              sendButton.addActionListener( new  SendingListener());
              
              panel.add( textSending );
              panel.add(sendButton);
              frame.add(panel);
              
              setUpNetWork();
              
              frame.setSize(400,400);
              frame.setVisible( true );
       }
       
        public  void  setUpNetWork() {
               try  {
                     Socket sock =  new  Socket( "127.0.0.1" ,4242);
              
                      writer  =  new  PrintWriter(sock.getOutputStream());
                     System. out .println( "Networking established" );
              }  catch  (IOException e) {
                      //  TODO  Auto-generated catch block
                     e.printStackTrace();
              }
       }
       
        public  static  void  main(String[] args) {
              MyGUI client =  new  MyGUI();
              client.go();
       }
       
        private  class  SendingListener  implements  ActionListener {

               @Override
               public  void  actionPerformed(ActionEvent arg0) {
                      //  TODO  Auto-generated method stub
                      writer .print( textSending .getText());
                      writer .flush(); 因为这里是不写满buffer 就发送
                     
                      textSending .setText( "" );
                      textSending .requestFocus();
              }
              
       }
}

多线程

上面的客户端只能自娱自乐,但是如果想要接收,并显示服务器消息该怎么弄呢?什么时候该去接收消息呢?用户点击发送的时候?那如果用户永远不发送,只是看呢?写一个大大的while(true)?那while的时候不干别的事情?我要是拖动滚动条(加入有的话),滚动条不会理我。

于是,多线程来了,怎么弄?
1、创建一个Runnable对象,就是所属类实现了Runnable接口。
Runnable threadJob = new MyRunnable();
注意:这块大概看一下就行,Runnable就是Thread的Job,这里先建立Job。
注意:这里可能疑惑,引用类型Runnable,new出来的是MyRunnable。因为MyRunnable实现了Runnable接口啊,多态多态。

2、 Thread myThread = new Thread(threadJob);
创建真正的线程,把线程动工作丢进去,就是那个Runnable对象。

3、 myThread.start();
正式开始线程。Runnable接口有个run方法,必须去实现,run方法就是线程启动时,第一个执行的方法,就好像是线程的main一样。

public class MyRunnable  implements Runnable {
      public void run() {
          go();
     }
     public void go() {
          doMore();
     }
     public void doMore() {
          System.out.println("I'm  doing more");
     }
}

class ThreadTester {
     public static void main (String[] args) {
          Runnable threadJob = new MyRunnable();
          Thread myThread =  new Thread(threadJob);

          myThread.start();

          System.out.println("back in main");
     }
}
Runnable接口,就一个方法,就是run()。
注意:上面代码要注意的地方是,main线程,和new 出来的新线程之间的执行顺序,没有保证,谁都可能先把字符串打出来。

注意:还有种实现是,写一个类,继承于Thread类,然后Thread类里面有个run函数,这个方法能用,但是并不好。用Runnable接口是,让job和Thread分开,本来就是两个工作,放到一起不是好实现,虽然语法等都是正确的。

注意:Thread类有个静态方法,Thread.sleep(2000),就是线程执行到这的时候进入休眠。线程醒来后,会进入runnable状态,等着运行,不会一醒来就运行。

try {
     Thread.sleep(2000);
} catch (InterruptedException ex) {
     ex.printStackTrace();
}
注意:Thread.sleep()这个函数是会报异常的,小心。

synchronized 关键字

讲到多线程,必然会牵扯到race condition。但是java有个好的地方,它把线程同步编织进了java语言当中,实现线程同步要简单的多。

race condition 是怎么回事就不说了。

如果一个函数的执行,你觉得会发生race condition,那就将它声明为synchronized。
private synchronized void makeWithdraw() {
     // doStuff
}

每一个java对象,自己天生都带一个锁,且就只有这一个。平时这个锁都是打开的,没人会注意到,但是当一个方法被声明为synchronized时,一个对象的线程想进入这个方法,必须保证拿到这个锁,用这种机制在保证不会出现线程之间的竞争。

注意:synchronized还保证这个方法称为原子方法,即上面写的那个doStuff,一个线程一旦拿到了锁,它必然会把它的stuff do完,才会把锁交出去,另一个线程才有可能进来。

这也是为了防止出现 lost update错误,如:
int i = balance;
balance = i + 1;
你即便写balance++;其实也是两步执行的。
上面这个在多线程的时候就容易出现错误,A B两线程,都是先读balance进入i,也就是说 i是读balance时候的值,不一定是执行瞬间的值。什么意思,就是说A读了个balance到i,然后睡着了,比如读的是5。没来得及加1呢。然后B来了,也读了个5,然后加了1,睡了。A又来了,又把balance加了个1,写的6。这时候该7了,但弄成6了,少了1。

注意:如果线程还没执行完,就睡着了,它会拿着锁睡,别的线程都干等。synchronized时候。

synchronized这么好,为什么不把所有方法弄成synchronized的?
答:
1、synchronized检查锁的时候会消耗时间。
2、一堆线程等在外面,等着拿锁需要消耗时间。
3、synchronized最致命的问题在有可能弄出来死锁。

如果不想把整个方法都弄成synchronized,也是有方法的。也就提一下:

对于static的方法能synchronized吗?
答:前面说锁跟对象相关,那static因为跟class有关,类synchronized后,static方法的锁该用哪个对象的呢?其实class也有自己的锁。即,如果有4个对象在堆上,实际上会有5个锁存在。访问static方法时候,用的就是这个类相关的锁。有多个static方法时,这多个static方法都使用这一个锁,即,一个线程在访问一个static方法时,另一个线程就无法访问static方法,即便不是前面那个线程正在访问的方法。

死锁

java中,没有处理线程死锁的工具,它甚至不会意识到死锁的存在。

所以,会不会死锁,全看你了。用设计保证,尽量别死锁。

第十三章 数据结构

从Collection API,我们可以看到,有 三个非常重要的接口,List、Set 和Map

List 在乎的是顺序,和各个元素的位置,重不重复不管。
Set 在乎的是里面元素有没有引用到相同元素(怎么定义相同,后面讲,不是说引用到同一个对象,是对象被认为相等)。
Map 是通过键值对,找元素, 内部引用的对象能重复,但关键字不许重复

Collection API的继承关系,精简版,省略很多类,写出来如下:
可以看出Map没有实现Collection接口,但也被认为是Collection框架下的。

除了ArrayList外,还有好多collection类,非常好用,比较常用的是下面几个:

TreeSet 保证元素被排序且没有重复

HashMap 让你能够通过关键字/值的方式,存取元素

LinkedList 链表,为了提高在中间部分读取和插入的速度,但是用的更多的还是ArrayList,因为只有数据比较大的时候才能明显些看出两者差距。

HashSet 禁止重复,且给出一个元素,可以在集合中快速找到这个元素

LinkedHashMap 类似HashMap,但元素是有序的,按插入顺序,或者最近访问顺序

注意:如果一个方法的参数为ArrayList<Animal> list,那只能传ArrayList<Animal>类型的list,不能是别的,ArrayList<Dog> list2是不行的。

TreeSet

当你想排序的时候,可以用sort()函数(下面说),也可以用TreeSet。元素插进去,就会自动排序。

但是她因为每次存,都要找位置插入,所以执行效率低一些。并且,大部分时候,你并不在意自己列表内元素的顺序,所以用的少。

java.util.Collections下,有个static的函数,sort(),用的时候Collections.sort()就行了,也好用呢。它的参数是List list,即List类型,什么ArrayList都集成于它。所以都能做参数。

泛型

我们基本不需要怎么写一个泛型类或方法,不过书上会说一些,因为泛型用的多,但是真的要自己写的少。都是用人家写好的。自己写函数的时候很少用到要写为泛型的。不像库里的collection 包内的函数,需要所有类都能用。

我们需要知道的是:
1、new ArrayList<Song> ()
创建泛型对象
2、List<Song> songList = new ArrayList<Song>();
创建、赋值一个泛型变量
3、void foo(List<Song> list)
声明一个方法的参数为泛型的。

泛型类声明:
public class ArrayList<E> extends AbstractList<E> implements List<E> {
     public boolean add(E o) {

          ...
     }
          ..............
}
上面E就和C++一样的。java一般也用T,这里是因为是Collection类,所以用E较多。

如果只是方法声明为泛型,即参数类型声明为泛型,两个方法:
1、public class ArrayList<E> extends AbstractList<E> implements List<E> {
     public boolean add(E o) {
即,使用声明类的时候,已经声明好的,泛型的类型。

2、public  <T extends Animal> void takeThing( ArrayList<T> list)
后面红的地方能那么写,能写T,是因为蓝色的地方声明的。

注意:有问题来了!很重要!
1、public <T extends Animal> void takeThing(ArrayList<T> list)
2、public void takeThing(ArrayList<Animal> list) 

是不同的。
1是只要是Animal的子类,我就能传参数给takeThing当参数,而2是必须是Animal类才行。

泛型重要相关 Comparable Comparator等

首先想说,rable和ator要看清啊 下面说的是两个东西。

先说Comparable接口

前面讲了,如果有个ArrayList<String> alist;里面放了好多的String,然后Collections.sort(alist),函数,可以对这个list里面的所有String,按照字母顺序进行排列。

但是,当我们自己做了个类,Song,里面是些歌曲相关的成员变量和方法。成员变量有好些歌名,歌手名等信息。然后我们做个新list ArrayList<Song> aNewList;里面放入很多很多的Song对象。

现在对这个aNewList进行排序,调用Collections.sort(aNewList);会发现直接报错。

首先就会想到,这不是个java自己的类,是我们写的,都没有规定按什么排序,怎么可能调用Collections.sort()函数呢?

我们去看Collections.sort()函数怎么写的,就看看声明就好:
public static <T extends Comparable<? super T>> void sort(List<T> list)

Comparable<? super T> 基本意思就是Comparable的类型可以是T,或者是T的父类。

根据前面的我们可以看出来,使用函数sort()时,声明的类型T,必须是继承于Comparable<? super T>的,我们刚才的那个Song必然不属于这个队列,这就是上面会报错的原因。

那么是不是说,我想要放到ArrayList里进行排序的对象,所属的类,必须都继承于Comparable<? super T>这个奇怪的类呢?

不是的,这里T extends Comparable<? super T>中的extends,也表示implements的意思。即, 在泛型中,extends既表示extends 也表示implements。

这里的Comparable其实是接口的意思。看看 String类的声明:
public final class String extends Object  implements Serializable, 
                                                        Comparable<String>, CharSequence

这也是String放入ArrayList后,能通过Collections.sort()函数进行排序的原因。

Comparable接口在java.lang.Comparable中,代码为:
public interface Comparable<T> {
     int compareTo(T o);
}

即,Comparable这个接口,只有一个要实现的函数,compareTo(),看了名字我们就明白了,各个String之所以能相互比较,谁大谁小怎么排,就是这个函数的原因,我们想要给Song类型的对象进行排序,也要通过这个函数来规定,怎么排,怎么才是大,怎么才是小。

使用这个compareTo(T o)函数时,这么用。
返回值为负数,表示当前对象,即this object,小于参数o对象;
返回正数,表示当前this 对象,大于参数传来的o对象;
等于,就表示,当前对象, 等于参数对象o。
注意:这里的等于不是说两个对象相等,是说这两个对象排序的时候没有先后。就像两个同名的歌曲一样,对象不一样,但排序没先后了,序一样。

public Song implements Comparable<Song> {
注意:类声明时候,你是Song类,就把接口Comparable<T>的T换成Song。
     String title;
     String artist;

     public int compareTo(Song s) {
          return title.compareTo(s.getTitle());
     }
      注意:这里直接调用String的compareTo()函数,因为title是String类型。
}

再来Comparator接口

如果上面的Song类,我现在改了,我又想要用artist来排序,或者说,我有时候想用title排序,有时候想用artist排序,怎么办?

我们看到上面方法一个接口只能实现一次,只能有一个compareTo()函数。但是,我们看到,Collections中,还有一个重载的sort()函数,声明为:
public static <T extends Comparable<? super T>> void sort(List<T> list ,      
                                                   Comparator<? super T> c )
比前面的sort()函数,多了一个参数。

这个Comparator<? super T>,也是一个接口,就是用于实现上面提到的情况的。该接口原型为:
public interface Comparator<T> {
     int compare(T o1, T o2);
}
在java.util.Comparator下。

这个函数的作用就是说,我调用sort函数的时候,如果通过重载,调用的是带有Comparator<? super T> c参数的sort(),那compareTo()函数就根本不会被调用,而是调用Comparator接口下的compare()函数,直接把前面说的compareTo()函数绕过去了。

注意:Comparator是个接口,所以Comparator<? super T> c参数,说的其实是任何实现这个接口的类的对象,都能当做参数传给sort(),多态多态。

只有调用sort(List<T> list ),即单参数版本的sort()时,才会调用compareTo()函数。

比如你现在手上有个类,该类没实现comparable接口,但是你没有这个类的代码,也改不成,这时候comparator接口就有用了,或者是继承刚那个没有comparable接口的类,然后让子类继承该接口,你就能使用compareTo()函数了。

于是, 举例,现在假如我正在写一个类Juke,这个类里有个方法是要对一堆Song对象排序,但是Song类的实现如前面所示,无法改动Song类,但我又想用artist来排序,我这么干:
现在这个Juke类里面,写一个inner class
class ArtistCompare implements Comparator<Song> {
     public int compare(Song one, Song two) {
          return one.getArtist().compareTo(two.getArtist());
           注意:这还调用的是String的比较函数。
     }
}

然后下面要排序的函数内,写:
ArtistCompare artistCompare = new ArtistCompare();
Collections.sort(songList , artistCompare);

这样,就可以在Song类不提供对artist排序的情况下,根据artist,对Song对象排序。

注意:compare(T one ,T two)与compareTo()差不多,返回值小于0,就是one比two大,其他的类似。

HashSet

讲了半天别的,现在回到HashSet。

如果有一大堆歌曲,然后里面很多首歌,歌名信息都一样,我想把这些重复信息去除,怎么做?HashSet来了。

首要问题是怎么定义两个对象是否相同?

HashSet工作的时候,每放入一个对象,都会拿他与HashSet中已有对象进行比较,比较他们的hashCode()值。

HashSet认为, 如果两个对象的hashCode()不同,那必然是两个不同的对象!
但是如果hashCode()值碰巧相同了,也不认为这两个对象就一定相同。

hashCode()相同的时候,HashSet还会自动调用equal()函数,继续进行比较。如果返回为true,他就终于相信两个对象时相同的了,就不允许这个对象加入到HashSet中。

注意:如果HashSet发现新加入对象,与已有对象有重复,add()函数返回false,并阻止新对象的加入。

注意:由于算法问题,hashCode()相同的时候,并不能保证两个对象是真的相同的,所以才出现了HashSet的那种机制。hashCode()也不光是用来判断两个对象是否相同,他还能提高Collections类,读取存储能力。比如HashSet在存对象的时候,就是根据hashCode(),来判断该把这个对象放在哪里的,这样对象存的非常多的时候也能很快找到位置,以便存储或者判断是否有重复。比如来了个对象hashCode 为500,HashSet一看,就立即去hashCode 为500的地方找,看有没有重复,不然假如像链表一样,得从第一个元素开始,一个一个比较看元素是不是500,那就慢多了。


所以,由上面的分析知道,如果我们想定义自己的标准,自己规定什么情况下,两个对象是相等的,不能放入HashSet,那我们就要重写hashCode() 和 equals()两个函数。不然 HashSet比较的时候调用默认的hashCode() equals(),两首歌歌名相同,但是是两个对象,那就也能放入HashSet,跟我们目标不同了。

把改动后的Song类写出来:
class Song {
     String title;
     String artist;
     String rating;

     public boolean equals(Object aSong) {
          Song s = (Song) aSong;
          return  getTitle().equals(s.getTitle());
     }
     public int hashCode() {
           return title.hashCode();
     }
}
注意:绿色和蓝色句,调用同一个成员变量的方式不同。蓝色地方我把getTitle()直接改为title,好像也没什么问题,我自己试验好的呢,不知道书中为什么这么写,还专门强调要看看这里写的不同。

注意:绿色句子有感觉到奇怪吗?我建立两个Song对象,A B。A有title,B也有title,A.hashCode()返回的是A中title的hashCode(),而B.hashCode()返回的是B中title的hashCode(),这两个怎么能一样呢?A B 创建的时候都是new出来的,是两个对象啊,这两个title应该是存在于两个对象上的,怎么hashCode()一样?
问题就在于Song中的title的声明是String的。
String a = new String("aaa");
String b = new String("aaa");
虽然a b是两个不同的引用,但是new String("aaa")语句,你传的参数都是"aaa",传的是引用,对"aaa"对象的引用,然后 java中,只要字符串一样的,都用的同一个对象。于是,就出现了a.hashCode() == b.hashCode()。
同理,上面return title.hashCode()为什么这么写了。不管你A B对象别的内容有多么的不同,只要存在title中的字符串一样,那返回的title.hashCode()就是一样的。所以,这样可以用于检测是否出现了重复的title。

TreeSet

刚才的HashSet,是无序的。即便把ArrayList比如,排好序,放进去,它也会瞬间变成乱序的。那么既要有序,又不能重复用什么呢?TreeSet来了。

TreeSet对象构建时,如果没有给参数,那添加元素进去的时候,是用的元素对象的compareTo()函数,进行比较。即要求这个对象得实现Comparable接口,但如果构建TreeSet对象的时候,给了它参数,给一个Comparator,那就能使用Comparator中的compare()函数进行比较。

典型示例:
TreeSet<Song> songSet = new TreeSet<Song>();
songSet.addAll(songList);
注意:假设这里songList为ArrayList类型,且已经装了很多Song对象。

注意:上面TreeSet对象构建时,没有参数。  new TreeSet<Song>(); 于是,必须要求Song这个类实现Comparable接口,完成compareTo()函数,不然可以过编译,但会出现Runtime错误。

注意:该实现Comparable接口的时候没有实现,编译不会报错!但运行出错!

如果有参数,示例如下:
SongCompare sCompare = new SongCompare();
TreeSet<Song> tree = new TreeSet<Song>(sCompare);
tree.addAll(songList);

这时候,实不实现Comparable接口都一样了,就是得写出SongCompare这个类。

HashMap

注意:Map中的键和值,其实是两个对象。也就是说,可以是任何对象。只是通常,我们把键的对象设置为String。

注意:Map中,键绝对不能重复,值随意。

使用举例:
HashMap<String , Integer> scores = new HashMap<String, Integer>();
看到了吗, <String, Integer>,一个给键,一个给值。

存入:
scores.put("Kathy",42);
scores.put("Bert",343);

取出:
System.out.println(scores);
System.out.println(scores.get("Bert"));

更多泛型重要相关,解释ArrayList<Animal>为参数时,为什么不能传ArrayList<Dog>类型对象

看个示例:
public void go() {
     Animal[] animals = {new Dog(), new Cat(), new Dog()};
      注意:多态原因,Animal数组中,我们能杂七杂八放入猫猫狗狗,没问题。
     Dog[] dogs = {new Dog() , new Dog() , new Dog() } ;
      注意:但是,如果是Dog数组,我们就放不进去Cat,因为是兄弟类的。
     
     takeAnimals(animals);
     takeAnimals(dogs);
}
public void takeAnimals(Animal[] animals) {
     for(Animal a : animals) {
          a.eat();
     }
}
注意:对于数组参数Animal[] animals,我投进去animals数组也行,即便是里面阿猫阿狗都有,投进去Dog数组也行,上面代码都能编译,并正常运行,因为多态。

注意:再次啰嗦,那个a.eat()当然是要求eat()必须在Animal类中声明了的。引用所能调动的函数,只和引用的类型有关,与其实际引用的对象无关。

接下来,使用泛型和多态:
public void go() {
     ArrayList<Animal> animals = new ArrayList<Animal>();
      注意:这里就是把前面的数组,变成了ArrayList。
     animals.add(new Dog());
     animals.add(new Cat());
     animals.add(new Dog());
      注意:将阿猫阿狗加入到ArrayList。

     takeAnimals(animals);
}
public void takeAnimals(ArrayList<Animals> animals) {
     for (Animal a : animals) {
          a.eat();
     }
}
注意注意注意!!!:这里的takeAnimals()也能正确运行!!你老以为不行。

注意:这里是先将阿猫阿狗放到ArrayList<Animal>中,再调用takeAnimals()的。就是说它本来就是ArrayList<Animal>类型,本来就是放Animal的ArrayList类型。 和下面不一样!

注意:即便上面takeAnimals()函数加入一句 animals.add(new Cat());也是可以的。即,改为:
public void takeAnimals(ArrayList<Animals> animals) {
     for (Animal a : animals) {
          a.eat();
     }
     animals.add(new Cat());
}
因为,编译器看得到,传的参数本来是个ArrayList<Animal>类型,不是别的,ArrayList<Dog>什么的。

注意: 这里前后关联着看吧,不然看不懂。看不明白的先看下面。回来就懂了。

但是,如果参数传的是ArrayList<Dog> dogs呢:
public void go() {
     ArrayList<Dog> dogs = new ArrayList<Dog>();
     dogs.add(new Dog());
     dogs.add(new Dog());

     takeAnimals(dogs);
}

public void  takeAnimals(ArrayList<Animals> animals)  {
     for (Animal a : animals) {
          a.eat();
     }
}
前面已经说过很多很多次了,这里会报错。

但为什么呢?耐心看下去:
数组类型,会检查两次,一次在编译时候,一次在运行时候;而collection类型,只在编译时检查一次!

上面这句话是问题的关键,慢慢解释。

看代码:
public void go() {
     Dog[] dogs = {new Dog(), new Dog() , new Dog()};
     takeAinmals(dogs);
}

public void takeAnimals(Animals[] animals) {
     animals[0] = new Cat();
}
即,本来是个Dog类型数组,经过多态,传到takeAnimals()函数中,但是这个函数里干了件事情,把数组中的第一个元素,改为了Cat类型对象。看上去像给animal数组中放了个Cat对象,但实际这个数组是Dog类型的,只是因为多态,伪装了一下而已。
上面代码在编译时候能正确运行,但是一旦启动,run时,会报错。这就是数组会检查两次的原因。

而ArrayList时候:
public void takeAnimals(ArrayList<Animal> animals) {
      animals.add(new Cat());
}
一旦出现上面语句,假如说能让你过编译,因为ArrayList只检查一次,那在本来是ArrayList<Dog>类型的对象中,加入Cat的事情就会发生了。即,给这个函数的参数传ArrayList<Dog> dogs; 

于是, java就干脆禁止这么传参数,写的ArrayList<Animal>,就只能传ArrayList<Animal>类型 ,Dog为其子类,也不能像多态那样,传ArrayList<Dog>。

<? extends Animal>

因为上面的禁令,所以产生了<? extends Animals>这种写法。

注意:extends和前面讲的一样,也是不但表示extends 还有 implements的意思。

这样,上面的takeAnimals()函数改为:
public void takeAnimals( ArrayList<? extends Animal> animals) {
     for (Animal a : animals) {
          a.eat();
     }
}
这样,这个函数,既能接收ArrayList<Animal> 类型的参数,也能接受ArrayList<Dog>或ArrayList<Cat>类型的参数,因为Dog Cat继承于Animal。

注意:但是,上面的takeAnimals()函数, 还是不允许有animals.add(new Cat())的行为,既不许有任何影响到ArrayList内部数据的行为。拿里面数据调用函数,或读值都行,别new对象添加进去。

注意:
public <T extends Animal> void takeThing(ArrayList<T> list)
和 
public void takeThing(ArrayList<? extends Animal> list)
功效是一样的。

只不过有时候,用T指代<? extends Animal>可能方便一点。比如:
public <T extends Animal> void takeThing(ArrayList<T> one, 
                                                       ArrayList<T> two)
就比写:
public void takeThing(ArrayList<? extends Animal> one , )
                                   ArrayList<? extends Animal> two)
方便一点。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值