第十二章 网络和线程
客户机方面
从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)
泛型重要相关 Comparable Comparator等
HashSet
后面红的地方能那么写,能写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)
方便一点。