2009-01-28

Java的分布式处理,引

  看了几天,终于把Head First Java看到了最后一章,虽然是最后一章,但是对于Java的学习来说,还只是序章的结束。
  这篇笔记将介绍Java 的RMI,Servlets,EJB和Jini,基本上这四个东西都至少需要一小本书才能弄得清楚,因此不可能在一章里面说完,书上只说了原理或是表象,而我也凭我的理解记下这篇笔记。当然,主要介绍的是RMI,因此RMI会比较详细一些。
那么,开始:

RMI (Remote Method Invocation)

RMI是Java最早的分布式处理的解决方案。

  设想一下情景:一台手机上的Java程序,需要处理一些大工作量的运算,自身机能有限,所以我们希望能够利用一台服务器来进行这种运算,而我的手机只需要提供原始数据和等待返回结果就可以(恩,是不是有点像云计算?)。那么,这种情况要怎么办?我们要调用另外一台JVM的堆上的对象的方法耶。。

  答案就是RMI来帮忙!

RMI的模型是这样的:

途中的箭头表示的是当客户端要调用远程服务时客户端和服务端的整个处理过程:
  首先我们应该明白各自的角色:
  在Client上,有Client对象和Client helper对象,在服务端有Service对象和Service helper对象。Client对象和Service对象是要进行RMI的对象。

  • Client Object:它希望能够调用远程服务器上的服务Service Object。
  • Service Object:它提供RMI的被调用对象。
  • Client helper:它负责实际的网络通信,它让Client Object感觉好像在调用本机的对象。我们看起来它似乎是在调用远程方法,但是实际上Client Object调用Client helper时只是在使用Client helper的Socket和流处理的功能,所以实际上Client helper是Service Object的代理。 ClientObject调用远程对象的方法时实际上是调用Client helper上的方法
  • Service helper:它通过Socket连接接受客户端的请求和信息并发给Service Object处理,在获得Service Object处理后返回的结果后又通过连接发送会给Client helper

  实际上,Client helper的真正的术语名称叫RMI stub,Service helper的真正术语名称叫RMI skeleton。这两个单词一个是存根一个是骨架,所以翻译了都不大能说明它真正的用途,还是别译的好。当然如果你非要理解成这个客户拿着存根去找那个被欺骗惨死后变成骨架的NPC作为任务结果道具要得到服务奖励的话我也没办法……
  在Java中,RMI会直接帮我们创建好stub和skeleton,不需要我们动手写它们的内容,但是,我们还必须注意的是,网络是会挂的因此RMI的stub和skeleton是会抛出异常的。
另外,在使用RMI时我们需要决定使用哪种协议:

  • RMI JRMP(Java Remote Message Protocol):这个协议很简单,也是Java RMI的原生协议,你在Java和Java程序之间进行RMI就只需要用这个协议就够了。
  • RMI-IIOP:这个协议是基于CORBA的,它可以让你远程的程序不是Java,有兴趣可以参考这篇比较老的文章:http://www.ibm.com/developerworks/cn/java/j-rmi-iiop/

接下来我们要说明一下如何创建一个RMI程序:

  1. 首先,我们需要创建一个Remote接口,定义客户端可以远程调用的方法。
  2. 之后,我们要实现这个接口。

  3. 接下来,用rmic对上一步写好的类进行处理,获得stub和skeleton

  4. 运行时,我们需要先在客户端进行rmiregistry(注意,这里的rmiregistry必须要运行在能够存取到你写的程序的目录上),再启动远程服务。

那么,我们开始进行详细的步骤:

第一步:创建一个Remote接口,定义客户可以远程调用的方法


TestRemote.java:
import java.rmi.*;

public interface TestRemote extends Remote {
public String sayHello() throws RemoteException;
}


  我们建立的这个Test Remote接口就是远程接口,它定义了所有客户端可以远程调用的方法,实现它的类将能够生成stub和skeleton,所以它是个作为服务的多态化类。它有几个小要求:

  • 此接口必须继承java.rmi.Remote接口,这个接口是一个标志性的接口,里面没有任何方法,但是这是RMI约定的规则,我们必须继承。
  • 我们写的这个接口的所有方法都必须抛出RemoteException,这是因为客户端会使用实现此接口的stub,而stub会进行网络通信和io操作,所以可能会发生异常,应此必须定义成能够抛出RemoteException
  • 这个接口定义的所有方法的参数和返回值都必须是primitive或Serializable类型(注意一个对象引用的所有对象都必须Serializable这个对象才能被Serialized)


第二步:实现上面定义的那个接口

TestRemoteImpl.java:
import java.rmi.*;
import java.rmi.server.UnicastRemoteObject;

public class TestRemoteImpl extends UnicastRemoteObject implements TestRemote {
public String sayHello() throws RemoteException {
return "Server says, 'Hey'";
}

public TestRemoteImpl() throws RemoteException { }

public static void main(String[] args) {
try {
TestRemote service = new TestRemoteImpl();
Naming.rebind("RemoteHello", service);
} catch(Exception e) {
e.printStackTrace();
}
}
}


  我们写的这个类就需要实现第一步中定义的TestRemote接口,为了要成为远程服务对象,我们写的这个类的对象必须要有与远程相关的功能,简而言之就是能够导出适当的远程对象与获得stub,因为我们使用JRMP,所以只需要简单的直接继承java.rmi.server.UnicastRemoteObject这个类即可。不过这个类有个问题就是它的构造函数会抛出RemoteException,所以我们最好的处理这个问题的方法就是声明一个会抛出同样异常的构造函数。

  好了,处理完这个,我们就已经有了可以远程调用的类了,那我们还要做什么才能使用RMI呢?
先从程序上考虑:

  • 显然我们需要一个TestRemoteImpl的对象
  • 为了让客户端能够找到我们的远程服务,显然还有注册成为网络服务,我们把TestRemote的对象使用静态的Naming.rebind方法将一个名字绑定到这个远程对象上,客户端只需要使用rmi://url/name就可以访问这个服务了。

我们再看客户端程序:

TestRemoteClient.java:
import java.rmi.*;
public class TestRemoteClient {
public static void main (String[] args) {
new TestRemoteClient().go();
}

public void go() {
try {
TestRemote service = (TestRemote) Naming.lookup("rmi://127.0.0.1/RemoteHello");
String s = service.sayHello();
System.out.println(s);
} catch(Exception e ) {
e.printStackTrace();
}
}
}


  这个客户端通过和服务器上的类相同的类型来引用到stub对象,Naming.lookup静态方法将返回这个stub对象的引用(实际上会对stub解序列化,因此客户端也需要stub),不过由于查询结果是Object类型,因此我们必须转换成接口类型。

  接下来客户端就可以像本地对象一样调用远程对象了。我们必须铭记的一点是,本地对象和远程对象互相传输的只有参数和返回结果。方法处理的过程都不会在客户端有任何影响。

  那么,在实际使用的过程中,我们一旦有了TestRemoteImpl.class就必须用rmic令其产生stub和skeleton(这个例子不会产生skeleton)。

  而在真正提供服务时,在服务器上我们先必须在可以存取到TestRemoteImpl类的目录下启动rmiregistry程序。之后才启动我们的远程程序。

最后,在RMI模型下,我们需要确保客户端和服务端要有这些类:

  • 客户端:TestRemote.class TestRemoteImpl_Stub.class
  • 服务端:TestRemote.class TestRemoteImpl.class TestRemoteImpl_stub.class TestRemoteImpl_Skel.class


  客户端是使用接口来调用stub上的方法,但是客户端不会再程序代码中引用到stub类,客户端总是通过接口来操作远程对象。服务端需要stub是因为它要将stub替换成连接在RMI registry上的真正服务。



-----------------------------------------------------------------------------------------------------------------------



EJB(Enterprise JavaBeans):

  对于大型企业级应用来说,RMI的功能可能是远远不够的,应此才有了Enterprise Application Server,EJB具有一系列RMI不具备的服务,比如交易管理、安全性、并发性、数据库等等,但是EJB也是和RMI有关系的,通常,EJB服务器作用于RMI调用和服务层之间。

它的部分原理图类似于下面这样:

这里的客户端其实可以是任何设备,不过通常是同一个J2EE服务器上的servlet。两端的通信还是RMI,不过区别就是到了服务器上后,RMI skeleton会把有业务逻辑的请求提交给EJB对象这个中间件,EJB Object获得业务逻辑的bean,再提交给entreprise bean,bean对象被限制只有服务器能够与bean沟通,这样增强了安全性和管理能力。

------------------------------------------------------------------------------------------------------------------------------------

Jini:
哎呀,终于到了传说中的Jini。Jini也是RMI的拓展。

Jini也可以使用RMI,并且拥有几个关键性的功能:

  • Adaptive Discovery(自适应搜索)
  • Self-healing Networks(自我恢复网络)

我们知道,RMI的客户端为了获得远程服务,它必须知道服务器的IP地址和服务在服务器上注册的名称,而Jini不需要,Jini客户端只需要知道服务所实现的接口就能够获得服务,这就是所谓的adaptive discovery。

Jini实现adaptive discovery的方法在于Jini的lookup service,Jini有一个提供服务的服务器,还有一个Jini查询服务。Jini查询服务上线的时候,它会在网络上广播寻找已经注册的Jini服务,找到后就会在Jini查询服务上注册。另一方面,如果是Jini服务先上线,Jini服务上线后会搜索网上的Jini查询服务并申请注册。注册时,服务会发送一个序列化对象给查询服务,向Jini查询服务注册这个对象所实现的接口。客户端要调用的时候,就会想Jini查询服务请求是否有服务实现某个接口。如果找到Jini查询服务就会返回这个序列化对象。

而Jini的self-healing networks的原理就是在Jini查询服务和Jini服务注册后,查询服务会定期要求更新租约,如果服务没有相应,那么Jini查询服务就会认为这个服务已经down掉。


------------------------------------------------------------------------------

更新:把RMI程序打包成jar

  在发布程序的时候,一般我们需要以jar包的形式发布出去,但是,我自己在使用的时候遇到了问题,运行时抛出了ClassNoFound:xxxx_stub.class这样的类似错误,在sun的java论坛询问过之后终于找到答案了。

  在运行rmiregistry的时候需要使用-J-classpath指定jar文件的类路径。

2009-02-20更新完毕

集合(Collections)与泛型(Generic)基础

我们先讨论集合

  在Java中,集合有这几种:List、Set和Map。从继承树上看,List和Set都是java.util.Collection的拓展接口。而Map则和java.util.Collection没有关系,但是它们都是集合。那么它们之间的区别呢?


  • List 又称为序列,就是有序的Collection,它能够保存索引位置。List可以有多个元素引用相同的对象。
  • Set 是一个不包含重复元素的的Collection
  • Map 是映射的集合,其数据是成对的,即键值(Key)和数据之(Value),Map中Key不允许重复,Value可以重复。

  而这几个集合都是接口,而接口加上其他的特性拼凑起来就是各种对应的集合,比如LinkedList就是链表List实现,HashSet就是基于哈希表的Set实现。

  可以看出,这三种主要的集合其中一个重要的区别就是是否容许重复的内容,但是这里的重复是如何判定的呢?

  这就涉及到引用相等和对象相等的问题:如果两个引用引用了同一个实例,那么它们就是引用相等,实际上就是同一个东西。而对象相等是两个引用引用了两个不同的实例,但是这两个实例在逻辑上被认为是相等的(比如两个int值都是22的Integer对象)。因此要判定是否相等就有比较复杂的过程了。

Java对对象的的相等性有指定一个规则(设两对象分别为A和B):

  • 两个对象相等⇔(A.hashCode() == b.hashCode() && A.equals(B)==0)
  • 两个对象有相同的hashCode()值,但未必相等。两个对象相等,则其hashCode()的值必然相等
  • hashCode()的默认行为是对heap上的对象产生独特的值,因此一个类的未override过的hashCode()方法在两个类的实例上一定产生不同的值。
  • equals()的默认行为是进行==比较,因此一个类的未override过的equals()方法用来测试两个类的实例一定不相等。

  以上是集合的基本信息,如果需要某些特别的功能的集合,可以从继承树上查看。




接下来是泛型(Generic):

  哈哈,高手说,不懂泛型的是假高手,懂了泛型的未必是高手。。。同学,恭喜你终于往高级的地方走了。。。。

  泛型这个特性直到Java5里才被加入。泛型能够使集合获得更好的类型安全性,当然,也有其他的好处。使用泛型编程的程序如果有类型问题,则它在编译阶段就会被编译器卡住,而不是等到执行时。对于我们初学者来说,在Java中最早见到的泛型的例子恐怕就是ArrayList的情况了。让我们看看ArrayList的类的定义:

public class ArrayList<e> extends AbstractList<e> implements List<e>, RandomAccess, Cloneable, Serializable

  这里有个<e> 显然很奇怪,不过我们可以把这个e当作集合所要处理的元素类型(element)。

  如果我们再看ArrayList的一个方法的定义我们就能更好的理解它了:

public boolean add(E o)



  这样结合起来不难理解。这个<e>所在的部分会被类使用时真正使用的类型所替代。这就是所谓的泛型。因此我们才能使用ArrayList ArrayList这样的语法来声明保存不同类型的ArrayList。

  除了上面那种声明外,还有两种使用泛型的声明方法:

public <T extends A> void TestClass(ArrayList<T> list)

public void TestClass(ArrayList<? extends A> list)


  这种声明法使得后面的T可以是任何一种A的子类(或者子接口,都是用extends)这两种方法是等价的,只是前一种先什么了以后后面可以直接用T来表示A的子类,而不需要每次都像后面那种写法写。


Head First Java在说明泛型的时候用的是一个对对象排序的例子,虽然脱离开它一样能够很容易的说明泛型,但是对像排序本身还是很常用的,所以值得一说。

------------------------------------------



对象排序

  假设我们要对一份歌曲列表进行排序,最简单的情况下我们只有歌曲名,那么,当使用歌曲名进行字母表排序的话,我们有很简单的方法:

  1. 用TreeSet读入歌曲名,TreeSet读入的对象会被自动按照顺序存储,因此只需要直接打印出来即可。

  2. 用ArrayList读入歌曲名,使用Collections.sort()方法来对ArrayList的元素进行升序排列。(Collections.sort()方法将改变ArrayList,事实上java.util.Collections提供了一系列静态方法用来处理集合)

考虑到性能,有时候我们必须要用到后一种方法,实际上,java.util.Collections.sort()这个方法使用合并排序算法,在Java5中文档说明,保证能提供nlog(n)的性能。

这个方法的原型是:
public static <T extends Comparable<? super T>> void sort(List<T> list)
我们要用的时候先声明一个List的子类对象,比如ArrayListArrayList<string> al = new ArrayList<string>();之后导入元素,要排序时只需要Collections.sort(al);这样就达成目的了。
  那么,如果现在我要排序的歌曲是一个自定义的对象,并且还要求要能够按照多种条件排序。这种时候要怎么办呢?显然,从上面sort方法的定义我们可以看出,sort能够排序的对象必须是实现了Cmparable接口的类对象。所以我们必须要在定义自己的类时实现这个接口。下面是一个Music的范例:

class Music implements Comparable<music> {
String title;
String artist;

public int compareTo(Music o) {
return title.compareTo(o.getTitle());
}

public Music(String t,String a) {
title = t;
artist = a;
}

public String getTitle() {
return this.title;
}

public String getArtist() {
return this.artist;
}
}

  这个Music类实现了Comparable接口,其中只有一个方法,compareTo,正式这个方法提供了sort对象进行排序的依据。因此只要把上面那个例子中的String对象直接换成Music就可以使用了。但是还有一个问题是我们不仅要按照歌名排序,还要按照歌曲作者排序。所以这种时候我们就应该用Collections的另外一个排序方法:

public static <t> void sort(List<t> list,Comparator<? super T> c)

  这个sort版本有两个参数,第一个还是要被排序的对象,第二个是一个Comparator的子类的对象。所以我们需要只需要新建一个实现了Comparator接口的类,然后调用这个Comparator的实例来排序即可。
Class ArtistCompare implements Comparator<music> {
public int compare(Music first, Music second) {
return first.getArtist().compartTo(second.getArtist());
}

这样就解决了排序的问题了。

简单的java网络程序

  由于封装掉底层的东西,所以要写一个简单的有网络通信功能的Java程序对于一个初学者来说还是比较简单的。
  首先我们需要了解一下Java的网络传输原理:
要使客户端能够工作,需要3个部分,当然,这不是tcp连接建立的3个过程。



  1. 客户端向服务器发起请求建立Socket连接
  2. 客户端传送信息给服务器
  3. 客户端从服务器接收信息

  我们从一个简单的聊天程序开始:先看程序,这个是客户端程序(我用绿色标明的是网络部分,蓝色标明的是多线程部分):
TalkBoxClient.java:

package talkbox;
import java.io.*;
import java.net.*;
import java.util.*;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class TalkBoxClient {
JTextArea incoming; //显示从服务器接收到的信息的文本域
JTextField outgoing; //输入要发送的信息的文本区
BufferedReader reader; //读取器缓冲
PrintWriter writer; //向服务器发送信息的文本发送器
Socket sock; //网络接口

public static void main(String[] args) {
TalkBoxClient client = new TalkBoxClient();
client.go();
}

public void go() {
/* ---------------------------GUI设置部分-----------------------------*/
JFrame frame = new JFrame("Simple talk box client");
JPanel mainPanel = new JPanel();
incoming = new JTextArea(15,50);
incoming.setLineWrap(true);
incoming.setWrapStyleWord(true);
incoming.setEditable(false);
JScrollPane qScroller = new JScrollPane(incoming);
qScroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
qScroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
outgoing = new JTextField(20);
JButton sendButton = new JButton("Send!");
sendButton.addActionListener(new SendButtonListener());
mainPanel.add(qScroller);
mainPanel.add(outgoing);
mainPanel.add(sendButton);
frame.getContentPane().add(BorderLayout.CENTER,mainPanel);
frame.setSize(400,500);
frame.setVisible(true);
/*---------------------------GUI配置结束------------------------------*/

setUpNetworking();

Thread readerThread = new Thread(new IncomingReader());
readerThread.start();

}

private void setUpNetworking() {
try {
sock = new Socket("127.0.0.1", 5000);
InputStreamReader streamReader = new InputStreamReader(sock.getInputStream());
reader = new BufferedReader(streamReader);
writer= new PrintWriter(sock.getOutputStream());
} catch(IOException e) {
e.printStackTrace();
}
}


public class IncomingReader implements Runnable {
public void run() {
String message;
try {
while ((message = reader.readLine()) != null) {
System.out.println("read " + message);
incoming.append(message + "\n");
}
} catch(Exception e) {
e.printStackTrace();
}
}
}


public class SendButtonListener implements ActionListener{
public void actionPerformed(ActionEvent ae) {
try {
writer.println(outgoing.getText());
writer.flush();
} catch(Exception e) {
e.printStackTrace();
}
outgoing.setText("");
outgoing.requestFocus();
}
}

}

先来分析下客户端程序,你现在可以只需要看绿色的部分:
显然,绿色的部分:
程序直接调用了setUpNetwork方法,而方法的定义:

sock = new Socket("127.0.0.1", 5000);
  这一句对Socket对象的引用sock调用构造函数使其指向一个新产生的Socket对象,这个Socket对象建立了本机与127.0.0.1:5000之间的网络连接。换句话说,其实在Java中,构造了一个Socket对象,JVM会自动处理它和服务器直接tcp连接的问题,我们无需关心,使用的时候只要当作一个普通的流对象来处理即可。 这样就实现了同服务器建立连接。

InputStreamReader streamReader = new InputStreamReader(sock.getInputStream());
  InputStreamReader是用来从Socket获得流的方法,我们新建了一个InputStreamReader对象来从Socket对象的getInputStream方法处获得流。这里方法的名字我们可以理解为都是相对于我们这个客户端程序来说,所以从服务器获得的流是InputStream。

reader = new BufferedReader(streamReader);
  reader是一个BufferedReader对象,就是一个缓冲型的读取器,一般读取写入数据的时候都会使用一个缓冲区来处理的。BufferedReader对应的写入器就是BufferedWriter了。

writer= new PrintWriter(sock.getOutputStream());
  这里用PrintWriter而不是用BufferedWriter的原因在于每次都是写入字符串,而PrintWriter能够提供方便的字符串写入方法。

  这样,通过reader和writer我们就成功的通过sock建立的连接来进行客户端和服务器的数据互相传输了。
  再看看SendButtonListener 这个实现了监听器接口的内部类。每当send按钮被点击时,客户端就会通过writer的方法
writer.println(outgoing.getText());
writer.flush();

向服务器立即写入一条数据。这样就实现了向服务器发送数据和获得数据了。

接下来看看服务端程序
TalkBoxServer.java:

package talkbox;
import java.io.*;
import java.net.*;
import java.util.*;

public class TalkBoxServer {
ArrayList clientOutputStreams;

public class ClientHandler implements Runnable {
BufferedReader reader;
Socket sock;

public ClientHandler(Socket clientSocket) {
try {
sock = clientSocket;
InputStreamReader isReader = new InputStreamReader(sock.getInputStream());
reader = new BufferedReader(isReader);
} catch(Exception e) {
e.printStackTrace();
}
}

public void run() {
String message;
try {
while((message = reader.readLine()) != null) {
System.out.println("read " + message);
tellEveryone(message);
}
} catch(Exception e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {
new TalkBoxServer().go();
}

public void go() {
clientOutputStreams = new ArrayList();
try {
ServerSocket serverSock = new ServerSocket(5000);

while(true) {
Socket clientSocket = serverSock.accept();
PrintWriter writer = new PrintWriter(clientSocket.getOutputStream());
clientOutputStreams.add(writer);


Thread t = new Thread(new ClientHandler(clientSocket));
t.start();

System.out.println("got a connection");
}
} catch(Exception e) {
e.printStackTrace();
}
}

public void tellEveryone(String message) {
Iterator it = clientOutputStreams.iterator();
while(it.hasNext()) {
try {
PrintWriter writer= (PrintWriter) it.next();
writer.println(message);
writer.flush();
} catch(Exception e) {
e.printStackTrace();
}
}
}
}


对于服务端来说,类似,我们也先只看绿色部分:

ServerSocket serverSock = new ServerSocket(5000);

  这句代码在本机的5000端口开启了一个服务端套接字对象,也就是监听了本机的5000端口用以应对客户的请求。

Socket clientSocket = serverSock.accept();

  在无限循环之中的这一句,调用了serverSock.accept()这个方法就等待连接。一旦有客户端连接了之前监听的端口的时候,这个方法就会返回一个新的Socket对象,返回的这个新的对象会建立服务器和客户端直接的套接字连接,但是服务器端的socket端口号被转成另外一个,以便于原来的端口能够继续监听等待客户端的请求,不过我们通过netstat命令看到的还是连接到5000的连接。由于这个方法在没有接收到连接之前会阻塞,也就是会让程序一直停在这里直到有客户端连接上,因此程序就不会无限的创建socket对象了。。。

  当然,这个只是为说明基本原理而存在的程序,我按照Head First Java里面的范例几乎没有修改的打过来的。这个程序几乎没有健壮性可言,但是用来说明问题已经足够。它是可以运行的。你可以试试。

  至于多线程部分,实际上客户端用多线程是为了能够同时向服务器发送信息和接收信息(你不会希望你的聊天程序在你不发信息的情况下收不到服务器的信息吧),因此while的条件

message = reader.readLine()

  这一句会一直等待套接字连接从服务器接收到信息。只要socket不挂它就能一直循环。

  而服务器端则是为了对每一个客户端建立一个Socket实例来向客户端读写信息。

2009-01-27

Java的序列化(serialization)

  不论如何,我们一定会遇到需要要保存状态的时候,一般,面对这种情况有两个比较好的选择:

  1. 将对象serialization,写入文件,之后只需要让程序从文件中对其进行deserialization即可,但是这种情况只适用于只有Java会读取这些数据。
  2. 以某种约定的方式,将某些状态信息写入文本文件

  好,现在考虑序列化的这种情况,一开始先介绍把序列化对象写入文件:

FileOutputStream fs = new FileOutputStream("outfile");

ObjectOutputStream os = new ObjectOutputStream(fs);

os.writeObject(object1);

//调用writeObject方法的时候对象就被serialization成流了。。

os.writeObject(object2);

os.close();

  好吧,过程很简单,对象先被碾平到ObjectOutputStream,之后ObjectOutputStream连接到FileOutputStream,然后FileOutputStream写入文件中。

  从上面的过程我们明白了,序列化只是把对象的实例变量的值压缩成流,保存在文件里而已。那么,现在问题来了,如果我的这个对象有其他对象的引用怎么办?比如有个字符串的实例变量。

  事实上这里面的问题是这样,首先,被序列化的对象必须要能还原会原来的状态,这是序列化存在的意义,第二,序列化对象展开之后存在的内存地址肯定不同了。由此两天可推得,一旦一个对象被序列化了,它所有的实例变量,以及它的实例变量引用的其他变量和这些变量再引用的东西,全部都会被自动序列化保存了。  但是,这里必须要注意的是,如果一个可以序列化的对象被序列化了,那他所包含的实例变量引用的对象也必须能够序列化,否则。额。信息不全显然是废物。。。

  在Java中,要是一个类能够序列化,就需要实现Serializable接口,这个接口其实没有任何方法,只是用以告知JVM这个类的对象是可以序列化的。当然,实现了这个接口只表示这个类的对象能被序列化,但是不一定要被序列化,如果在某种情况下这个对象不需要被序列化,那么只需要加上transient标记即可,比如String类的对象:

import java.net.*;

class test implements Serializable {

transient String temp;//序列化时会以null保存

String storage;//序列化时将被序列化

}

了解了序列化之后,接下来是解序列化,同样,首先还是看它的过程:

FileInputStream fs = new FileInputStream("infile");
ObjectInputStream os = new ObjectinputStream(fs);

//ObjectImputStream负责从数据流中deserialization出对象
Object one = os.readObject();

//每执行一次能够读取出一个被serialization的对象,这个过程同之前序列化的过程一样,如果读取的次数超过拥有的对象个数会抛出异常,readObject方法返回的对象是Object类型的
Object two = os.readObject();
SpecifyClass scone = (SpecifyClass) one;//通过强制类型转换获得对象的引用

SpecifyClass sctwo = (SpecifyClass) two;

os.close();//os关闭时fs也会自动关闭

解序列化有些规则:

  • JVM会通过存储的信息判断出对象的类型
  • 当JVM试着加载对象的类的时候如果JVM无法找到或加载这个类,JVM也会抛出异常
  • 被解序列化的对象的构造函数是不会执行的。但是,如果这个对象有个不可序列化的父类,那么这个这个父类及其在继承树上的所有父类的构造函数都将被执行。因此,对于一个存在不可序列化的父类的可序列化的类来说,它的这些父类的资料全部都会被初始化。
  • 静态变量是不会被序列化的

Java的数字格式化和日期的处理

数字格式化:
  注意,这个是数字的格式化而不是数字的格式化输出。
  显然,格式化之后的数字就不再是数字,而是一个字符串对象了。在Java 5之后,java.uitl.Formatter这个类提供了很多不错的格式化功能,但是简单的数字格式化可以直接用java.lang.String.format()这个静态方法实现。这个格式化规则很类似于C的printf
简要介绍一下语法:
String.format("test1 %[argument number][ flags][width][.precision]type test2" , number);
每个%代表一个参数,这和C的printf类似,而且它也是用可变参数列表实现的。
这种数字格式化不仅支持数字显示的格式化,日期、时间也可以,而且还有重复使用参数的方法。
比如
String.format("test1 %,d %,.2f test %x, var1,var1,var1);
只需要改写成
String.format("test1 %,d %<,.2f test %<x, var1)也及时加个<表示重复利用之前的参数。


操作日期:

在Java 5中,我们仍然使用java.util.Date来获得当前日期,但是如果要进行比较复杂的日期操作的话就要用java.util.Calendar了
Calendar对象可以设定日期,可以加减运算,可以将日期用毫秒表示。
虽然java.util.Calendar是一个abstract 类,但是我们要获得Calendar的实例却有简单的方法:
Calendar cal = Calendar.getInstance();
Calendar.getInstance()是一个静态方法,而之所以能这样用是因为这个静态方法会返回一个java.util.Calendar的一个子类的实例,通常是java.util.GregorianCalendar。这样我们就可以通过使用java.util.Calendar的方法来操作日期了。

构造器(constructor)和垃圾收集器(garbage collector)

  要了解Java的对象在整个运行过程中的存在情况,就必须先了解堆(heap)和栈(stack),对Java来说,Java虚拟机启动后,就能够从操作系统中取得一块内存区域来运行Java程序。  对于Java来说,堆和栈的区别在于,Java的所有对象都存在于可垃圾回收的堆上,也就是之前JVM从系统中取得的内存区域。而Java程序的方法调用和局部变量则存在于栈上。这篇笔记假设你已经知道栈的工作形式。
  从栈的工作原理,我们可以分析出程序执行的情况:当一个对象被创建,那它就存在于堆上,JVM将为这个对象提供足以保存所有实例变量的内存空间(如果实例变量是primitive时就是该primitive需要的空间,如果是对象的引用时纯粹保存引用)。当调用了对象的方法时,该方法和方法的局部变量(也称栈变量)被压入栈顶,因此,不断调用方法栈顶就不断更新,根据栈的工作形式我们知道,某一时刻栈只有最顶层才能被访问,因此,局部变量在栈上和它所属的方法在同一层中,其作用范围就仅在该方法在栈顶的时候。
  理解了上面的内容,就不难理解构造器和垃圾回收器了。

构造器:
  目前为止我听说过在Java里唯一称为函数的就是一个类的构造函数了,那么接下来列出构造函数的逻辑特征:

  • 构造函数名和类名相同,access level可以是public,可以不指定,也可以是private。
  • 地球人都知道,如果我们不写构造函数,编译器也会自己创造一个。但是当你写了个有参数的构造函数时,编译器就不会帮你弄个无参数的出来了。
  • 构造函数签名的区别仅仅在于参数表的不同。
  • 一个类的实例变量尽管没有在构造函数里初始化,它也有默认值:0 0.0 false 和null。但是局部变量嘛,鬼才知道。。。

  接下来是构造函数运行时的特征,当然,这就又牵扯到了继承关系上了:假设一个类B继承自类A,类A继承自Object类。
那么,当类B被实例化时,调用了类B的构造函数,我们知道类B肯定包含了类A和类Object的部分,显然,这样就需要运行类A和Object类的构造函数来构造出它们的实例,那么,这几个类的关系是如何的呢?
先从程序上来说:假设类B是:
public class B extends A {
  int test;
  public B() {
    test = 11;  }
}

那么,编译器会自动把它当成
public class B extends A {
  int test;
  public B() {
    super();
    test = 11
  }
}

来编译。显然,B的构造函数先调用了父类A的构造函数。
  这种变化的规则是,如果你没有写,那编译器一定在构造函数的第一行补上,如果你写了,那你写的super()也只能存在于构造函数的第一行。显然,这个super()调用的是无参数版本的父类构造函数。根据我们上面说的栈的特征。结果就显然了,第一个被完成的构造函数就是Object类的构造函数,因此,被实例化的顺序显然就是Object→A→B了。显然这也符合逻辑性,毕竟,儿子不可能比老子早出生。
  另外,值得一提的是,一个类的构造函数也可以调用自己的其他构造函数,但同时也要满足两个要求,this()和super()只能同时存在一个并且this()也必须放在第一行。当然,这里的this()和super()指的就是自己或者父类的构造函数,因此可以是有参数的版本比如:
this(false);
super(11);

这样的。

最后就是垃圾收集器了:
  我们都知道,一个对象的所有引用丢失的话,那么它就会被垃圾收集器视为垃圾,也就不可用了。因为我们可以对唯一的引用赋null或者把唯一的引用指向其他对象,不过还需要注意的是,当对象的引用是局部变量,也就是在栈上的时候,随着这个引用的生命周期结束,那该对象也将自动被消费,所以有时离开栈必死的局部变量的问题啦。

再谈Java中的类、抽象类和接口还有Object类

  放假了,趁着这个机会温故而知新,在读书的过程中,理一理自己的知识体系。唉,悲叹一下由于上个学期把大部分时间花在计算机技术上了,没有花多少时间在本专业的课程上,导致下个学期要达到我的目标需要付出大量的时间了。。。。引以为戒啊。
 
  刚才读的是Java的多态,Head First Java的作者引出概念的方法相当高明,让人浅显易懂,概括起来:

类与抽象类:
  本来,一个类继承了父类,那么就表明,在这个继承体系中,父类比较抽象,子类比较具体一点。但是在很多情况下,不应该存在父类的实例,也就是父类不应该被实例化。就像有具体的各种动物,但是一个叫做animal的实例不应该存在。因此就有必要有抽象类。它不能被实例化。
  从程序的规则上来说,一个类中,只要有一个抽象的方法就必须把这个类声明为抽象类,而一个抽象类中除了抽象的方法也可以有有实现的方法。另外,一个抽象方法必须要有最终实现它的子类的对应的方法。

类与接口:
  在一群继承自同一个父类A的子类中,有一部分不仅和A有着IS-A的关系,还和另外一个类B有着IS-A的关系,这种时候我们不应该把A和B合并,因为这样导致那些没有B特征的A特征的子类也需要被迫继承B的特性,所以只能让那些同时和AB有着IS-A关系的子类同时继承A和B两个类,但是这个时候难题就出现了,如果这些同时和AB有着IS-A关系的子类同时继承A和B两个类的话,会出现多重继承的很多麻烦的问题,在JAVA中,也不允许多重继承,但是在Java中替代的解决方案就是interface。
  一个类可以继承自一个父类,但同时允许实现多个接口,而不同继承树的类可以实现同样的接口。这里的接口可以看作是纯粹的抽象类,其所有方法都是抽象的。


所以,在设计中,判别继承树的时候可以通过考虑一下问题来确定使用的是抽象类还是接口:

  • 如果要定义一群子类的模板(因此模板不应该被初始化),就使用抽象类
  • 如果要定义出类可以扮演的角色,就使用接口。

  了解了以上关系,Java的整个多态性的结构就初具雏形了。这里就很自然的引出了另外一个问题:Java的终极类——Object类在整个Java中的作用。

  众所周知,Object类是Java中所有类源头。它的存在,使得程序员可以写出能够处理所有类型的通用类。但是有趣的是,Object不是抽象类,它允许被创建出实例,Object的主要目的在于作为多态,让方法可以应付多种类型;提供Java在运行时对任何对象都有需要的方法的实现。

  但是,显然我们不应该极端到随便把参数和返回类型设定为Object,因为一个被认定为Object的引用的实例是无法调用非Object类所有的方法的。当然我们可以通过强制类型转换来处理,但是这样太暴力了。。。很可能导致程序健壮性上的问题。。。

2009-01-26

程序设计过程

  唉,中国的山寨手机几乎无敌了,大年初一的就在电视上乱七八糟的广告那个傻逼“金XX”手机,吵死了。。。
  
  这篇文章主要在于介绍一种Java的程序设计的过程的方法论,当然,不可能复杂只是一点读书心得而已。
  这里介绍的是head first Java中提到的一种。它要求在设计类的过程先编写伪代码,再根据伪代码思考程序测试用的测试代码,最后再来编写真实程序。
  伪代码基本大家都熟悉,提到测试代码,我们也许会考虑为何要先写出测试代码再写程序,这是因为在编写测试代码的过程中,我们能够更了解并且准确定位于我们要编写的类需要做哪些是。当然,一般还是先写出一点测试代码,之后写一点与之对应的程序,一点一点测试,保证所有代码的最终正确性!
  据称,这个概念来自于所谓的极限编程(Extreme Programming or XP)我们可以参考这个网页的流程图来了解它:
http://www.extremeprogramming.org/map/project.html
当然,其主页有详细的介绍
http://www.extremeprogramming.org/index.html

2009-01-25

Windows7上可用的虚拟光驱程序

  平常使用Windows的时候还是非常需要用虚拟光驱的,比较现在下载的很多资源都是以光盘镜像的形式存在的,因此有必要安装一个,不过很遗憾的是不管是DaemonTools还是Alcohol 120%在Windows 7 下都无法正常使用,究其原因,我测试的结果看起来似乎是SPTD没能成功的安装导致的,因为SPTD(SCSI Pass Through Direct)是一种比较底层的驱动程序,它能够优先控制硬件,因此和操作系统的关系比较大,现在还不支持Windows 7 吧。这样以来基于它的DaemonTools和Alcohol 120%都不能正常使用了,所以只能换别的程序了。   

根据网友的使用情况,目前比较推荐的两款程序分别是:Virtual CloneDrive和WinMount,前者比较单纯的是虚拟光驱的软件,后者本质上来说是一款压缩打包文件的软件,它所谓独创的mount技术,我们可以考虑为:做一个isz文件,用daemon tool挂载上直接读取使用即可。。虽然这个软件可能有自己独创的技术,不过我比较喜欢单纯的东西,所以我选择了前者。
  Virtual CloneDrive是免费软件,直接到官方网站下载即可:http://www.slysoft.com/en/download.html

最后还是感叹一下,Linux的那种机制只需要把iso文件mount就可以用了,真是方便啊。。。希望Windows 7 正式版能够直接提供挂载iso文件的程序,就不需要这么麻烦了。。。

------------------------------------------------
更新:
截至今日,最新的DaemonTool已经支持windows7

2009-01-23

Google的一个在线Ajax API调试页面

  貌似是最近才出现的。可以用它直接在线测试Google的Ajax的API,稍微玩弄了几下。左上角可以直接从分类中找到你要的Ajax代码,右边是源码,下面是预览,直接修改源码再点预览窗的run就可以显示你修改的结果了,相当好用的一个在线测试工具。
地址:http://code.google.com/apis/ajax/playground/

2009-01-20

为Ubuntu 8.10装上Wiimote

  今天在Matt Cutts的博客上读到了这篇好文,哈哈,有钱买Wiimote的朋友赶快试试:
  简单来说,如果你的电脑有蓝牙功能,直接用,没有的话准备一个蓝牙适配器。接下来
sudo apt-get install wminput wmgui lswm
  然后在图形程序中配置好就OK,如果你有兴趣全文阅读可以到上面的那个链接去读读原文。

Google Notebook阵亡了,你的笔记本下一个新家在哪儿?

  实话说,就算像Google所说的那样,Google Notebook可以用Google Docs和其他服务整合在一起实现同样的效果,但是对像我这样比较喜欢搜集自己阅读的信息中觉得不错的一些的人来说,原来Google Notebook使用上的和原信息完整的功能保留是很重要的。因此,在Google宣布Google Notebook阵亡了的情况下,我们必须要不得以的为我们的笔记本找个新家》》》
  显然,Google觉得这个在线剪贴板服务不怎么又前途,不过别人不这么认为,目前提供类似服务的还有EverNote,Zoho Notebook, UberNote 和Diigo。而且为了抢夺市场,在Google notebook事件公布后,他们也都在快马加鞭的准备接受这部分用户,不仅都开发了Google Notebook importer,还到处发关键词广告。如果你最近看到这篇文章的话,注意一下底下的AdSense广告,说不定就有他们的广告。。。
  基本上,由于现在不急,我等有闲情了在来对比测试一下这几个在线剪贴板服务的好坏,不过目前显而易见的是,Zoho看起来还不会阵亡,而且添加了不少新功能,不过我不大喜欢它整合了大量的社会化功能。

关于固态硬盘(SSD)

  虽然未必会买,但是至少,现在的我们应该对这一种on the way的技术要有一个比较清楚的认识。最近比较简短有不错的一篇关于固态硬盘的文章就是那个Intel工程师回答关于固态硬盘的问题的文章了:
  固态硬盘是近年来的热门话题,但有关这种和传统硬盘完全不同的存储介质,恐怕很多人心中都还存有不少疑问。HardOCP网站日前请到了Intel固态硬盘工程师Jonathan Schmidt,解答了普通用户提出的许多有关固态硬盘使用中的问题。虽然其中很多都属于入门级问题,但相信大多数人看完仍会有所收获。

外部使用环境会如何影响固态硬盘?
由于没有活动部件,固态硬盘比传统硬盘更加抗冲击和震动。另外,由于不使用磁性存储介质,也不会有被磁化导致数据丢失的危险。因此,笔记本制造商如果使用固态硬盘,可以省去很多的硬盘防震保护配件,进一步节约机身内空间和重量。对于桌面PC来说,使用固态硬盘更是不需要担心任何使用环境问题。
  有人问到机场安检透视扫描仪是否会影响固态硬盘,这是一个相当有趣的问题,我并不能给出一个权威答案。但要知道,固态硬盘从物理特性来看和U盘、存储卡、手机中的闪存没有什么区别,因此应当不需要担心X射线会对其产生影响。

如何保证固态硬盘的可靠性?
首先,闪存是一项成熟技术,经过了长期的实际测试。虽然闪存颗粒有一定的读写寿命,但以目前的技术来说,其寿命已经远远高于实际使用年限。比如,Intel固态硬盘的官方数据显示,无论使用频度高低,它最少也有5年的有效使用期。如果应用频率不高的话还可以再延长5年。另外,Intel固态硬盘内置了ATA SMART监控功能,随时可以查看其健康状况。用户可以放心,数据安全绝对是固态硬盘制造商的第一考量。

为什么没有3.5寸的固态硬盘?
最主要的原因是,“合理容量”的闪存从物理规格上来看占不了太大空间,做成2.5寸或1.8寸规格更合适。这里我说的“合理容量”是指能够提供实际应用中足够的存储空间,同时价格较为合理。如果将闪存装满一个3.5寸硬盘位,其价格肯定相当惊人。
  很多人可能会对此有误解,认为SSD没有3.5寸型号是因为它只针对笔记本市场。实际上,固态硬盘从未排斥过桌面PC,在台式机的3.5寸硬盘位中安装2.5寸固态硬盘没有任何难度。而且,3.5寸和2.5寸硬盘的SATA接口也没有任何区别。

固态硬盘需要整理磁盘碎片么?
这个问题的答案比较复杂。固态硬盘的数据存储方式和传统硬盘有明显的区别,比如为了防止频繁读取某存储单元而导致快速老化,固态硬盘往往使用“损耗平衡”机制,将读写各个区块的次数平均化。目前的操作系统对此也没有准备。
  磁盘碎片整理程序的主要原理是,将那些需要频繁读取的数据放在可以高速访问的地方,很少访问的数据就堆在边边角角。而固态硬盘的原理决定,它能够非常快速的找到任何一块数据。目前的磁盘整理工具对优化固态硬盘的文件系统就显得无能为力了。因此,我的建议是,固态硬盘用户应当禁用自动磁盘碎片整理,也不要手动进行整理。
  当然,对于固态硬盘来说也同样存在存储分布的优化问题,只是这个问题在SSD上远不如传统硬盘那么重要。目前,各固态硬盘厂商都在用固件优化的形式解决这一问题。未来也可能会出现专门针对固态硬盘的“碎片整理”工具,不过它需要首先了解各厂商固态硬盘的具体工作方式。

固态硬盘会越用越慢么?
这是一个复杂的问题。在SSD的寿命周期中,很多因素都会影响它的性能表现。其中最重要的就是数据碎片问题。很不幸,目前尚无任何方法从外部衡量固态硬盘的数据破碎程度的影响。就像上面说的一样,测试程序也许能够检测出固态硬盘内部存储条理与否的性能差别,但这并不会明显影响用户体验。对固态硬盘文件系统的优化未来还将进一步解决这一问题。

Intel固态硬盘支持热插拔么?
没问题,完全支持SATA规范定义的热插拔功能。

Intel固态硬盘使用怎样的制程工艺?
X18-M和X25-M使用的是Intel 50nm MLC闪存,而X25-E使用的是50nm SLC闪存。

当固态硬盘被装满的时候,性能会下降么?
很好的问题。对于固态硬盘来说,性能和存储数据的多少没有什么关系。无论空空如也还是接近爆满,闪存的损耗均衡管理算法都会照常工作。一些常见文件系统如NTFS、FAT32在空间不足时可能会出现性能下降,但这是软件的问题,和是否使用固态存储没有关联。未来当专门针对固态硬盘的文件系统问世时,可能也会出现硬盘存储数据量多少对性能的影响的例子。

哪种文件系统最适合固态硬盘?
目前的的各种文件系统都没有对固态硬盘进行什么优化。计算机行业花了几十年的时间,针对旋转磁介质存储进行优化,但固态硬盘的出现让这些优化彻底作废。幸运的是,以目前固态硬盘的速度,遵循旧文件系统的要求像传统硬盘那样工作,并不会有太大的损失。不过在不远的将来,我们肯定将看到专为固态硬盘优化的文件系统。
  微软在Windows 7中就将对SSD进行优化,比如系统会在使用固态硬盘时禁用自动磁盘碎片整理功能。其中我最关注的是ATA trim命令,它能够通知固态硬盘,某区块已经不再使用,SSD可以将其空间收回,纳入下一步的“损耗平衡”运算中。
  在Linux系统中,你可非常简单的通过禁用内核disk IO scheduler模块来对固态硬盘进行优化。由于不存在磁头读写的移位问题,该模块在磁盘读写时进行重新排序对固态硬盘没有任何意义,甚至会降低性能。Windows 7估计也会进行同样的改进,只是目前还未公布。

固态硬盘RAID 0的性能怎样?可以在SSD内部实现RAID 0么?
先来回答第二个问题。固态硬盘的读写本身就是并行进行的,目前Intel固态硬盘使用10条并行通道来访问闪存,一定意义上就相当于内置10路RAID 0。
使用多块固态硬盘组建RAID 0阵列的性能相当可观,但需要注意的是,一定要保证RAID控制器能够满足其要求。固态硬盘在阵列模式下工作的数据量相当庞大,很多RAID控制器在设计时可能完全没有考虑过这样的速度。

固态硬盘速度的决定因素是什么?目前的瓶颈在哪里?
任何固态硬盘的性能,都是由原始的闪存带宽,损耗平衡算法的效率(固件)以及接口(SATA、PCI-E等)共同决定的。有SATA接口速度卡在那里,闪存性能再强也没有意义。和业界其他厂商一样,我们也将逐步提升固态硬盘性能。虽然不能说固态硬盘在“赶着”SATA-III标准上马,但一旦第三代SATA标准推出,固态硬盘肯定会从中受益。

SSD和HDD相比有何优劣?
和其他任何事情的两面一样,SSD和HDD各有优劣。目前固态硬盘最大的劣势就是成本和容量,而最大的优势就是性能。另外,固态硬盘完成相同的操作所需的电能更少,这意味着笔记本可以延长电池续航时间,数据中心能够大大节约电费。由于更加耐震动冲击,固态硬盘也比HDD更适合移动设备。如果容量需求不高的话,固态硬盘甚至可以比传统硬盘更便宜。比如目前售价最低的上网本基本上都是使用小容量固态硬盘。
  下面我们来具体看固态硬盘的性能优势,简单比较数据会让你忽略掉很多东西。比如,Intel X25-M硬盘的持续读取速度为250MB/s,一块常见SATA硬盘则为100MB/s,从字面上来看SSD速度是HDD的2.5倍。这时你就忽略随机访问时间的问题。X25-M的平均“寻道时间”仅为85微妙,而传统硬盘大多在4到15毫秒,差距达到50甚至150倍。
  因此,两者的性能区别要视应用而定。操作系统启动主要依赖随机读取小块数据,因此固态硬盘可比传统硬盘快100倍。而在应用程序连续读取大尺寸文件时,固态硬盘的优势就只有2.5倍左右了。
  同时,仍有一些应用并不适合固态硬盘,比如大规模数据存档。那些极少访问的数据用闪存来存储显然是一种浪费。另外,在视频播放时使用固态硬盘也不会有任何优势,只要达到视频不卡壳的速度需求就可以了。只不过,HTPC用户可能会青睐固态硬盘的静音和尺寸。

为什么大家都用MLC颗粒,SLC不是更快么?
没错,SLC NAND闪存更快,但只有在面对面比较的时候才能看到明显区别。而且,只要大规模使用并行读写机制,MLC同样可以实现高速度。在这样的情况下,SSD厂商肯定会更加关注成本和容量问题,MLC的低价大容量就成了优势。我想大家都看到了,Intel的M系列固态硬盘使用的就是MLC颗粒,不是照样很快么?

固态硬盘的功耗相比传统硬盘孰高孰低?
我曾看过一些报告宣称固态硬盘比传统硬盘更费电,但也有一些调查显示SSD更省电。通常来看,SSD和HDD在同样高负载工作,或同样处在休眠状态下时,功耗是类似的。但固态硬盘仍然在功耗表现上有一些优势,比如SSD内部没有旋转马达,因此在闲置状态时的功耗明显更低。第二,由于不存在转速提升或下降的启动时间,SSD进入休眠状态或从休眠状态唤醒的时间更短,也更频繁。最后,固态硬盘能够在更短时间内完成同样的工作,因此更早进入休眠状态。以上这些优势让固态硬盘在实际使用中确实比传统硬盘省电。

是否存在不同等级的闪存?为什么U盘比同样容量的固态硬盘便宜的多?
确实,闪存有不同的质量,对应不同的成本,就像CPU一样。U盘一般使用较低档次的闪存,如果你把U盘当作硬盘来使用,我想你马上就能感受到性能差别。另外在可靠性上,优劣闪存的区别也是明显的,高质量的闪存芯片在整个寿命周期内的出错几率要低得多。虽然我们完全可以用廉价闪存造出便宜的固态硬盘,但便宜没好货的道理我想大家都是明白的。