转载至:http://blog.csdn.net/shakespeare001/article/details/51388516


作者:山代王(开心阳)

1、super的作用

    在Java中super指代父类对象(直接父类),也就是说,super相当于是一个直接new出来的父类对象,所以可以通过它来调用父类的那些非private修饰的变量、方法(对于我们普通new出来的对象来说,也就只能访问那些非private的成员变量、方法了,这里的访问是指通过“对象名.变量名或方法名”的形式)。所以,super这个对象也就是一个普通对象,同样遵循访问控制修饰符的准则。
    然而,对于子类来说,子类通过继承就直接拥有了父类的非private变量、方法,也就可以在子类中直接使用,再加一个super来修饰,岂不是显得有点多余了?正常情况下来说,是有点多余了(但是可以明确提示我们这是调用的父类变量或方法),但super关键字主要是用在以下两种情况中:
(1)发生了重写的情况
    重写也分为两种情况,一个是重写了父类的方法;一个是重写了父类的成员变量;
重写父类方法的情况
  1. public class A {
  2. String name = ”lly”;
  3. protected void getName(){
  4. System.out.println(”父类getName->”+ name);
  5. }
  6. }
  7. public class B extends A {
  8. String nameB = ”llyB”;
  9. @Override
  10. protected void getName() {
  11. System.out.println(”子类getName->”+nameB);
  12. super.getName();
  13. }
  14. public static void main(String[] args) {
  15. B b = new B();
  16. b.getName();
  17. }
  18. }
public class A {
String name = "lly";
protected void getName(){
System.out.println("父类getName->"+ name);
}
}
public class B extends A {
String nameB = "llyB";
@Override
protected void getName() {
System.out.println("子类getName->"+nameB);
super.getName();
}
public static void main(String[] args) {
B b = new B();
b.getName();
}
}
打印如下:
子类getName->llyB
父类getName->lly

在子类B中,我们重写了父类的getName方法,如果在重写的getName方法中我们去调用了父类的相同方法,必须要通过super关键字显示的指明出来。
如果不明确出来,按照子类优先的原则,相当于还是再调用重写的getName()方法,此时就形成了死循环,执行后会报java.lang.StackOverflowError异常。

重写父类变量的情况
我们将B类简单改造一下:
  1. public class B extends A {
  2. String name = ”llyB”;
  3. @Override
  4. protected void getName() {
  5. name = super.name;
  6. System.out.println(”子类getName->”+name);
  7. }
  8. public static void main(String[] args) {
  9. B b = new B();
  10. b.getName();
  11. }
  12. }
public class B extends A {
String name = "llyB";
@Override
protected void getName() {
name = super.name;
System.out.println("子类getName->"+name);
}
public static void main(String[] args) {
B b = new B();
b.getName();
}
}
此时子类B中有一个和父类一样的字段(也可以说成父类字段被隐藏了),为了获得父类的这个字段我们就必须加上super,如果没有加,直接写成name
= name;不会报错,只是会警告,表示此条语句没有任何意义,因为此时都是访问的子类B里面的那么字段。

我们通过super是不能访问父类private修饰的变量和方法的,因为这个只属于父类的内部成员,一个对象是不能访问它的private成员的。

(2)在子类的构造方法中
编译器会自动在子类构造函数的第一句加上
super(); 来调用父类的无参构造器;此时可以省略不写。如果想写上的话必须在子类构造函数的第一句,可以通过super来调用父类其他重载的构造方法,只要相应的把参数传过去就好。

因此,super的作用主要在下面三种情况下:
1、调用父类被子类重写的方法;
2、调用父类被子类重定义的字段(被隐藏的成员变量);
3、调用父类的构造方法;
其他情况,由于子类自动继承了父类相应属性方法,关键字super可以不显示写出来。

2、关于构造方法

如果一个类中没有写任何的构造方法,JVM会生成一个默认的无参构造方法。在继承关系中,由于在子类的构造方法中,第一条语句默认为调用父类的无参构造方法(即默认为super(),一般这句话省略了)。所以当在父类中定义了有参构造函数,都是没有定义无参构造函数时,IDE会强制要求我们定义一个相同参数类型的构造器。这也是我们在Android中自定义组件去继承其他View是经常被要求定义几个构造函数的原因。

以下子类B的情形是错误不能通过编译的:
  1. public class A {
  2. public A(String s){  }
  3. }
  4. public class B extends A {    //编译错误,JVM默认给B加了一个无参构造方法,而在这个方法中默认调用了super(),但是父类中并不存在该构造方法
  5. String name = ”llyB”;
  6. }
  7. public class B extends A {    //同样编译错误,相同的道理,虽然我们在子类中自己定义了一个构造方法,但是在这个构造方法中还是默认调用了super(),但是父类中并不存在该构造方法
  8. String name = ”llyB”;
  9. public B(String s){}
  10. }
public class A {
public A(String s){ }
}
public class B extends A { //编译错误,JVM默认给B加了一个无参构造方法,而在这个方法中默认调用了super(),但是父类中并不存在该构造方法
String name = "llyB";
}
public class B extends A { //同样编译错误,相同的道理,虽然我们在子类中自己定义了一个构造方法,但是在这个构造方法中还是默认调用了super(),但是父类中并不存在该构造方法
String name = "llyB";
public B(String s){}
}
此时就需要显示的去调用父类构造方法了,如下:
  1. public class B extends A {    //正确编译
  2. String name = ”llyB”;
  3. public B(String s){
  4. super(s);
  5. }
  6. }
public class B extends A {    //正确编译
String name = "llyB";
public B(String s){
super(s);
}
}
所以,只要记住,在子类的构造方法中,只要里面没有显示的通过super去调用父类相应的构造方法,默认都是调用super(),即无参构造方法,因此要确保父类有相应的构造方法。

3、transient关键字用法

当用transient关键字修饰一个变量时,这个变量将不会参与序列化过程。也就是说它不会在网络操作时被传输,也不会再本地被存储下来,这对于保护一些敏感字段(如密码等…)非常有帮助。

当我们一个对象实现了Serializable接口,这个对象的所有字段和方法就可以被自动序列化。当我们持久化对象时,可能有一个一些特殊字段我们不想让它随着网络传输过去,或者在本地序列化缓存起来,这时我们就可以在这些字段前加上transient关键字修饰,被transient修饰变量的值不包括在序列化的表示中,也就不会被保存下来。这个字段的生命周期仅存在调用者的内存中。
如下一个例子:
  1. public class UserBean implements Serializable{
  2. private static final long serialVersionUID = 856780694939330811L;
  3. private String userName;
  4. private transient String password;    //此字段不需要被序列化
  5. public String getUserName() {
  6. return userName;
  7. }
  8. public void setUserName(String userName) {
  9. this.userName = userName;
  10. }
  11. public String getPassword() {
  12. return password;
  13. }
  14. public void setPassword(String password) {
  15. this.password = password;
  16. }
  17. }
public class UserBean implements Serializable{
private static final long serialVersionUID = 856780694939330811L;
private String userName;
private transient String password; //此字段不需要被序列化
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
测试类:
  1. public class Test {
  2. public static void main(String[] args) {
  3. UserBean bean = new UserBean();
  4. bean.setUserName(”lly”);
  5. bean.setPassword(”123”);
  6. System.out.println(”序列化前—>userName:”+bean.getUserName()+“,password:”+bean.getPassword());
  7. //下面序列化到本地
  8. ObjectOutputStream oos = null;
  9. try {
  10. oos = new ObjectOutputStream(new FileOutputStream(“e:/userbean.txt”));
  11. oos.writeObject(bean);//将对象序列化缓存到本地
  12. oos.flush();
  13. } catch (FileNotFoundException e) {
  14. e.printStackTrace();
  15. } catch (IOException e) {
  16. e.printStackTrace();
  17. }finally{
  18. if(oos != null){
  19. try {
  20. oos.close();
  21. } catch (IOException e) {
  22. e.printStackTrace();
  23. }
  24. }
  25. }
  26. //下面从本地反序列化缓存出来
  27. try {
  28. ObjectInputStream ois = new ObjectInputStream(new FileInputStream(“e:/userbean.txt”));
  29. bean = (UserBean) ois.readObject();
  30. ois.close();
  31. System.out.println(”反序列化后获取出的数据—>userName:”+bean.getUserName()+“,password:”+bean.getPassword());
  32. } catch (FileNotFoundException e) {
  33. e.printStackTrace();
  34. } catch (IOException e) {
  35. e.printStackTrace();
  36. } catch (ClassNotFoundException e) {
  37. e.printStackTrace();
  38. }
  39. }
  40. }
public class Test {
public static void main(String[] args) {
UserBean bean = new UserBean();
bean.setUserName("lly");
bean.setPassword("123");
System.out.println("序列化前--->userName:"+bean.getUserName()+",password:"+bean.getPassword()); //下面序列化到本地
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(new FileOutputStream("e:/userbean.txt"));
oos.writeObject(bean);//将对象序列化缓存到本地
oos.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally{
if(oos != null){
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
} //下面从本地反序列化缓存出来
try {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("e:/userbean.txt"));
bean = (UserBean) ois.readObject();
ois.close();
System.out.println("反序列化后获取出的数据--->userName:"+bean.getUserName()+",password:"+bean.getPassword());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
打印结果如下:
序列化前—>userName:lly,password:123
反序列化后获取出的数据—>userName:lly,password:null

看到,password反序列化后的值为null,说明它没有被保存到本地,因为我们给它加上了transient修饰。

注意:
(1)从上面可以看到,被transient修饰后,反序列化后不能获取到值;
(2)transient只能修饰变量,不能修饰方法,修饰我们自定义的对象变量时,这个对象一定要实现Serializable接口;
(3)static变量不能被序列化,即使它被transient修饰。因为static修饰的变量是属于类的,而我们序列化是去序列化对象的。
上面的例子中如果给userName加上static修饰,反序列化后依然能够获取到值,但是这个时候的值是JVM 内存中对应的static的值,因为static修饰后,它属于类不属于对象,存放在一块单独的区域,直接通过对象也是可以获取到这个值的。上面的第三点依然成立。

4、下面哪些类可以被继承

下面哪些类可以被继承? Java.lang.Thread、java.lang.Number、java.lang.Double、java.lang.Math、 java.lang.ClassLoader
A、Thread    B、Number    C、Double    D、Math    E、ClassLoader

正确答案:ABE
1、对于Thread,我们可以继承它来创建线程;
2、Byte、Short、Integer、Long、Float、Double几个数字类型都是继承自Number;
3、Byte、Short、Integer、Long、Float、Double、Boolean、Character几个基本类型的包装类,以及String、Math它们都是被定义为public final class,因此这几个都不能被继承;
4、我们可以自定义类加载器来实现加载功能,因此ClassLoader是可以被继承的。

5、for和foreach遍历的比较

    针对for和foreach两种循环,我用10万、100万、1000万级别大小的List集合数据进行了测试一下,发现整体来说foreach的执行时间和普通for循环的时间差别不大。对于这两个的比较,我网上查了一下,有说foreach效率高一点的,有说for效率高一点的。
    网上说for效率高的,主要是在针对遍历集合类的数据时,for表现要稍微好一点,因为foreach内部它使用的是Iterator迭代器的执行方式。因此下面分两种结构来测试:
先来测试对数组遍历的时间快慢,数组里面保存1000万的随机数。如下: 
      
  1. List<Integer> list = new ArrayList<Integer>();
  2. Integer[] list = new Integer[10000000];
  3. for(int i = 0; i < 10000000; i++){//先生成1000万随机数
  4. list[i] = Math.round(100000);//随机数只是咋100000以内
  5. }
  6. long size = list.length;
  7. long start = System.currentTimeMillis();
  8. int a;
  9. for(int i = 0; i < size; i++){
  10. a =list[i];
  11. }
  12. long end = System.currentTimeMillis();
  13. System.out.println(”耗时—>”+(end-start));
  14. 经过多次运行,发现平均耗时7ms左右。将上面的for循环改成foreach形式,如下:
  15. for(Integer i : list){
  16. a = i;
  17. }
List<Integer> list = new ArrayList<Integer>();
Integer[] list = new Integer[10000000];
for(int i = 0; i < 10000000; i++){//先生成1000万随机数
list[i] = Math.round(100000);//随机数只是咋100000以内
}
long size = list.length;
long start = System.currentTimeMillis();
int a;
for(int i = 0; i < size; i++){
a =list[i];
}
long end = System.currentTimeMillis();
System.out.println("耗时--->"+(end-start));
经过多次运行,发现平均耗时7ms左右。将上面的for循环改成foreach形式,如下:
for(Integer i : list){
a = i;
}
经过多次运行后,发现平均耗时5ms左右。
再来测试遍历集合List的情况:
  1. List<Integer> list = new ArrayList<Integer>();
  2. for(int i = 0; i < 10000000; i++){
  3. list.add(Math.round(100000));
  4. }
  5. long size = list.size();
  6. long start = System.currentTimeMillis();
  7. int a;
  8. for(int i = 0; i < size; i++){
  9. a =list.get(i);
  10. }
  11. long end = System.currentTimeMillis();
  12. System.out.println(”耗时—>”+(end-start));
  13. 多次运行后,平均运行时间为21ms左右,将上面的for循环改成foreach形式,如下:
  14. for(Integer i : list){
  15. a = i;
  16. }
List<Integer> list = new ArrayList<Integer>();
for(int i = 0; i < 10000000; i++){
list.add(Math.round(100000));
}
long size = list.size();
long start = System.currentTimeMillis();
int a;
for(int i = 0; i < size; i++){
a =list.get(i);
}
long end = System.currentTimeMillis();
System.out.println("耗时--->"+(end-start));
多次运行后,平均运行时间为21ms左右,将上面的for循环改成foreach形式,如下:
for(Integer i : list){
a = i;
}
多次运行后,平均运行时间为24ms左右。

经过上面一些简单的实验,我们发现,两种方式在遍历数组的时候时间普遍都较快,在遍历集合的时候耗时会更久一些。在遍历数组时,foreach的表现要稍微好一点,在遍历集合的时候,for的表现要好一点。但是不管哪种情况,for和foreach这两种遍历方式时间都相差不大。因此对于这两者的比较在时间效率来说应该相差不会很大(上面没有测试复杂数据的情况,以及其他集合结果的情况,可能不准确)。主要是在对于两者的应用场景上的选择:
(1)普通for循环可以给定下标,因此当我们需要这个信息时,我们可以选用普通for循环来操作遍历;
(2)foreach在代码结构上更加清晰、简单;
(3)foreach在遍历的时候会锁定集合中的对象,期间不能修改,而for中可以修改集合中的元素。

6、Java IO与NIO

(本小节内容大部分来源于参考文章)
Java NIO(Java non-blocking IO)是JDK1.4以后推出的,相对于原来的IO来说,Java NIO是一种非阻塞的IO方式,它为所有的基本类型提供了缓存支持。
一般来说,I/O操作包括:对硬盘的读写、对socket的读写以及外设的读写。
阻塞和非阻塞:

阻塞:当某个事件或者任务在执行过程中,它发出一个请求操作,但是由于该请求操作需要的条件不满足,那么就会一直在那等待,直至条件满足;

非阻塞:当某个事件或者任务在执行过程中,它发出一个请求操作,如果该请求操作需要的条件不满足,会立即返回一个标志信息告知条件不满足,不会一直在那等待。

  这就是阻塞和非阻塞的区别。也就是说阻塞和非阻塞的区别关键在于当发出请求一个操作时,如果条件不满足,是会一直等待还是返回一个标志信息。

阻塞IO和非阻塞IO:

当用户线程发起一个IO请求操作(本文以读请求操作为例),内核会去查看要读取的数据是否就绪,对于阻塞IO来说,如果数据没有就绪,则会一直在那等待,直到数据就绪;

对于非阻塞IO来说,如果数据没有就绪,则会返回一个标志信息告知用户线程当前要读的数据没有就绪。当数据就绪之后,便将数据拷贝到用户线程,这样才完成了一个完整的IO读请求操作。

也就是说一个完整的IO读请求操作包括两个阶段:

  1)查看数据是否就绪;

  2)进行数据拷贝(内核将数据拷贝到用户线程)。

  那么阻塞(blocking
IO)和非阻塞(non-blocking IO)的区别就在于第一个阶段,如果数据没有就绪,在查看数据是否就绪的过程中是一直等待,还是直接返回一个标志信息。

  Java中传统的IO都是阻塞IO,比如通过socket来读数据,调用read()方法之后,如果数据没有就绪,当前线程就会一直阻塞在read方法调用那里,直到有数据才返回;而如果是非阻塞IO的话,当数据没有就绪,read()方法应该返回一个标志信息,告知当前线程数据没有就绪,而不是一直在那里等待。

同步IO和异步IO:

从字面的意思可以看出:同步IO即 如果一个线程请求进行IO操作,在IO操作完成之前,该线程会被阻塞;

  而异步IO为 如果一个线程请求进行IO操作,IO操作不会导致请求线程被阻塞。

  事实上,同步IO和异步IO模型是针对用户线程和内核的交互来说的:

  对于同步IO:当用户发出IO请求操作之后,如果数据没有就绪,需要通过用户线程或者内核不断地去轮询数据是否就绪,当数据就绪时,再将数据从内核拷贝到用户线程;

  而异步IO:只有IO请求操作的发出是由用户线程来进行的,IO操作的两个阶段都是由内核自动完成,然后发送通知告知用户线程IO操作已经完成。也就是说在异步IO中,不会对用户线程产生任何阻塞。

  这是同步IO和异步IO关键区别所在,同步IO和异步IO的关键区别反映在数据拷贝阶段是由用户线程完成还是内核完成。所以说异步IO必须要有操作系统的底层支持。(即同步IO是用户线程不断的轮询、有数据之后进行拷贝,而异步IO是内核完成这两个步骤,与用户线程无关。)

阻塞IO和非阻塞IO是反映在当用户请求IO操作时,如果数据没有就绪,是用户线程一直等待数据就绪,还是会收到一个标志信息这一点上面的。也就是说,阻塞IO和非阻塞IO是反映在IO操作的第一个阶段,在查看数据是否就绪时是如何处理的。

注意同步IO和异步IO与阻塞IO和非阻塞IO是不同的两组概念,同步IO和异步IO考虑的是由哪个线程(用户线程or内核线程)来完成IO的处理,而阻塞IO和非阻塞IO,针对的是IO操作中的第一个阶段的处理方式,是一直等待还是直接返回状态信息。


五种IO模型:
见下面第一篇参考文章

Java NIO:
Java NIO中比较核心的三个概念是:通道(Channel)、缓冲区(Buffer)、选择器(Selector)。
(1)通道(Channel):
通道和传统IO中的Stream类似,但传统IO中的Stream是单向的,比如InputStream只能进行读(read)操作,OutputStream只能进行写(write)操作,而NIO中的通道是双向的,既可以进行读也可以进行写操作。常用的有以下几种通道:
  • FileChannel  –从文件读或者向文件写入数据
  • SocketChanel   –以TCP来向网络连接的两端读写数据
  • ServerSocketChannel   –服务器端通过ServerSocketChanel能够监听客户端发起的TCP连接,并为每个TCP连接创建一个新的SocketChannel来进行数据读写
  • DatagramChannel   –以UDP协议来向网络连接的两端读写数据

(2)缓冲区(Buffer):
Buffer实际上是一个容器,是一个连续数组。Channel提供从文件、网络读取数据的渠道,使用Java NIO时,所有数据处理(读取或写入)必须经由Buffer。即读入数据时,先通过通道Channel获取数据渠道,然后将数据写入到缓冲区Buffer中,往外写数据时,先把内存中的数据放到Buffer中,然后再从Buffer送往通道中。如下图所示:

NIO 为Java的所有基本类型都提供了缓存对象,如ByteBuffer、IntBuffer。。。

一个传统IO读写数据的例子: 
  
  1. public static String readFromStream(InputStream is) throws IOException{
  2. ByteArrayOutputStream baos = new ByteArrayOutputStream();
  3. byte[] buffer = new byte[1024];
  4. int len = 0;
  5. while((len = is.read(buffer))!=-1){
  6. baos.write(buffer, 0, len);
  7. }
  8. is.close();
  9. String result = baos.toString();
  10. baos.close();
  11. return result;
  12. }
public static String readFromStream(InputStream is) throws IOException{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while((len = is.read(buffer))!=-1){
baos.write(buffer, 0, len);
}
is.close();
String result = baos.toString();
baos.close();
return result;
}
传统IO在读取数据时,直接从InputStream中读入到byte[]数组中,如is.read(buffer),在写数据时,直接将内存数据写到输出流中,如baos.write(buffer,0,len);

使用NIO读取数据:按照上图,读取数据时,从通道中将数据读入到Buffer中,写数据时,将内存数据先写到Buffer,再送入通道。
    NIO 读取数据: 
      
  1. FileInputStream in = new FileInputStream(“e:\\lly.txt”);
  2. FileChannel fileChannel = in.getChannel();
  3. //创建一个ByteBuffer缓冲区
  4. ByteBuffer buffer = ByteBuffer.allocate(10);
  5. //从通道读入数据到缓冲区
  6. fileChannel.read(buffer);
  7. //重设此缓冲区,将限制设置为当前位置,然后将当前位置设置为0
  8. buffer.flip();
  9. while(buffer.hasRemaining()){
  10. byte b = buffer.get();//从缓冲区中取数据到内存中
  11. System.out.print(((char)b));
  12. }
  13. in.close();
FileInputStream in = new FileInputStream("e:\\lly.txt");
FileChannel fileChannel = in.getChannel();
//创建一个ByteBuffer缓冲区
ByteBuffer buffer = ByteBuffer.allocate(10);
//从通道读入数据到缓冲区
fileChannel.read(buffer);
//重设此缓冲区,将限制设置为当前位置,然后将当前位置设置为0
buffer.flip(); while(buffer.hasRemaining()){
byte b = buffer.get();//从缓冲区中取数据到内存中
System.out.print(((char)b));
}
in.close();
    NIO写数据:       
  1. //模拟数据
  2. byte[] data = { 83, 111, 109, 101, 32,
  3. 98, 121, 116, 101, 115, 46};
  4. FileOutputStream out = new FileOutputStream(“e:\\lly.txt”);
  5. FileChannel fileChannel = out.getChannel();
  6. //创建一个ByteBuffer缓冲区
  7. ByteBuffer buffer = ByteBuffer.allocate(20);
  8. //先将数据写入到Buffer中
  9. for(int i = 0; i < data.length; i++){
  10. buffer.put(data[i]);
  11. }
  12. //重设此缓冲区,将限制设置为当前位置,然后将当前位置设置为0
  13. buffer.flip();
  14. //把buffer中的数据送入到通道中
  15. fileChannel.write(buffer);
  16. out.close();
     //模拟数据
byte[] data = { 83, 111, 109, 101, 32,
98, 121, 116, 101, 115, 46};
FileOutputStream out = new FileOutputStream("e:\\lly.txt");
FileChannel fileChannel = out.getChannel();
//创建一个ByteBuffer缓冲区
ByteBuffer buffer = ByteBuffer.allocate(20);
//先将数据写入到Buffer中
for(int i = 0; i < data.length; i++){
buffer.put(data[i]);
}
//重设此缓冲区,将限制设置为当前位置,然后将当前位置设置为0
buffer.flip();
//把buffer中的数据送入到通道中
fileChannel.write(buffer);
out.close();
可以看到,Java NIO在数据处理方面需要通过Buffer来缓存,与传统IO处理方式相比,NIO相当于是数据块处理,传统IO是一个一个字节处理。其实,在传统IO后面出现了BufferInputStream、BufferOutputStream这种也是缓存数据块的处理方式。
同时,我们看到在读或写时都调用了Buffer的buffer.flip();这个方法非常重要,它是用来设置缓冲对象的一些状态信息,在读get()和写put()之前,都需要调用buffer.flip();来设置状态。

关于flip的具体作用请参考这篇文章,非常具体形象(http://blog.csdn.net/wuxianglong/article/details/6612246)。下面的文章来自这篇文章:
”————–引用————-

在第一篇中,我们介绍了NIO中的两个核心对象:缓冲区和通道,在谈到缓冲区时,我们说缓冲区对象本质上是一个数组,但它其实是一个特殊的数组,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况,如果我们使用get()方法从缓冲区获取数据或者使用put()方法把数据写入缓冲区,都会引起缓冲区状态的变化。本文为NIO使用及原理分析的第二篇,将会分析NIO中的Buffer对象。

在缓冲区中,最重要的属性有下面三个,它们一起合作完成对缓冲区内部状态的变化跟踪:

position:指定了下一个将要被写入或者读取的元素索引,它的值由get()/put()方法自动更新,在新创建一个Buffer对象时,position被初始化为0。

limit:指定还有多少数据需要取出(在从缓冲区写入通道时),或者还有多少空间可以放入数据(在从通道读入缓冲区时)。

capacity:指定了可以存储在缓冲区中的最大数据容量,实际上,它指定了底层数组的大小,或者至少是指定了准许我们使用的底层数组的容量。

以上四个属性值之间有一些相对大小的关系:0 <= position <= limit <= capacity。如果我们创建一个新的容量大小为10的ByteBuffer对象,在初始化的时候,position设置为0,limit和 capacity被设置为10,在以后使用ByteBuffer对象过程中,capacity的值不会再发生变化,而其它两个个将会随着使用而变化。四个属性值分别如图所示:

现在我们可以从通道中读取一些数据到缓冲区中,注意从通道读取数据,相当于往缓冲区中写入数据。如果读取4个自己的数据,则此时position的值为4,即下一个将要被写入的字节索引为4,而limit仍然是10,如下图所示:

下一步把读取的数据写入到输出通道中,相当于从缓冲区中读取数据,在此之前,必须调用flip()方法,该方法将会完成两件事情:

1. 把limit设置为当前的position值 

2. 把position设置为0

由于position被设置为0,所以可以保证在下一步输出时读取到的是缓冲区中的第一个字节,而limit被设置为当前的position,可以保证读取的数据正好是之前写入到缓冲区中的数据,如下图所示:

现在调用get()方法从缓冲区中读取数据写入到输出通道,这会导致position的增加而limit保持不变,但position不会超过limit的值,所以在读取我们之前写入到缓冲区中的4个自己之后,position和limit的值都为4,如下图所示:

在从缓冲区中读取数据完毕后,limit的值仍然保持在我们调用flip()方法时的值,调用clear()方法能够把所有的状态变化设置为初始化时的值,如下图所示:

(3)选择器(Selector)

Selector类是NIO的核心类,Selector能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。这样一来,只是用一个单线程就可以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。如下如所示:

要使用Selector,得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新连接进来,数据接收等。
如下的一个完整例子,例子来源于(http://weixiaolu.iteye.com/blog/1479656
  1. package cn.nio;
  2. import java.io.IOException;
  3. import java.net.InetSocketAddress;
  4. import java.nio.ByteBuffer;
  5. import java.nio.channels.SelectionKey;
  6. import java.nio.channels.Selector;
  7. import java.nio.channels.ServerSocketChannel;
  8. import java.nio.channels.SocketChannel;
  9. import java.util.Iterator;
  10. /**
  11. * NIO服务端
  12. * @author 小路
  13. */
  14. public class NIOServer {
  15. //通道管理器
  16. private Selector selector;
  17. /**
  18. * 获得一个ServerSocket通道,并对该通道做一些初始化的工作
  19. * @param port  绑定的端口号
  20. * @throws IOException
  21. */
  22. public void initServer(int port) throws IOException {
  23. // 获得一个ServerSocket通道
  24. ServerSocketChannel serverChannel = ServerSocketChannel.open();
  25. // 设置通道为非阻塞
  26. serverChannel.configureBlocking(false);
  27. // 将该通道对应的ServerSocket绑定到port端口
  28. serverChannel.socket().bind(new InetSocketAddress(port));
  29. // 获得一个通道管理器
  30. this.selector = Selector.open();
  31. //将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后,
  32. //当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。
  33. serverChannel.register(selector, SelectionKey.OP_ACCEPT);
  34. }
  35. /**
  36. * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
  37. * @throws IOException
  38. */
  39. @SuppressWarnings(“unchecked”)
  40. public void listen() throws IOException {
  41. System.out.println(”服务端启动成功!”);
  42. // 轮询访问selector
  43. while (true) {
  44. //当注册的事件到达时,方法返回;否则,该方法会一直阻塞
  45. selector.select();
  46. // 获得selector中选中的项的迭代器,选中的项为注册的事件
  47. Iterator ite = this.selector.selectedKeys().iterator();
  48. while (ite.hasNext()) {
  49. SelectionKey key = (SelectionKey) ite.next();
  50. // 删除已选的key,以防重复处理
  51. ite.remove();
  52. // 客户端请求连接事件
  53. if (key.isAcceptable()) {
  54. ServerSocketChannel server = (ServerSocketChannel) key
  55. .channel();
  56. // 获得和客户端连接的通道
  57. SocketChannel channel = server.accept();
  58. // 设置成非阻塞
  59. channel.configureBlocking(false);
  60. //在这里可以给客户端发送信息哦
  61. channel.write(ByteBuffer.wrap(new String(“向客户端发送了一条信息”).getBytes()));
  62. //在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。
  63. channel.register(this.selector, SelectionKey.OP_READ);
  64. // 获得了可读的事件
  65. else if (key.isReadable()) {
  66. read(key);
  67. }
  68. }
  69. }
  70. }
  71. /**
  72. * 处理读取客户端发来的信息 的事件
  73. * @param key
  74. * @throws IOException
  75. */
  76. public void read(SelectionKey key) throws IOException{
  77. // 服务器可读取消息:得到事件发生的Socket通道
  78. SocketChannel channel = (SocketChannel) key.channel();
  79. // 创建读取的缓冲区
  80. ByteBuffer buffer = ByteBuffer.allocate(10);
  81. channel.read(buffer);
  82. byte[] data = buffer.array();
  83. String msg = new String(data).trim();
  84. System.out.println(”服务端收到信息:”+msg);
  85. ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes());
  86. channel.write(outBuffer);// 将消息回送给客户端
  87. }
  88. /**
  89. * 启动服务端测试
  90. * @throws IOException
  91. */
  92. public static void main(String[] args) throws IOException {
  93. NIOServer server = new NIOServer();
  94. server.initServer(8000);
  95. server.listen();
  96. }
  97. }


  1. package cn.nio;
  2. import java.io.IOException;
  3. import java.net.InetSocketAddress;
  4. import java.nio.ByteBuffer;
  5. import java.nio.channels.SelectionKey;
  6. import java.nio.channels.Selector;
  7. import java.nio.channels.SocketChannel;
  8. import java.util.Iterator;
  9. /**
  10. * NIO客户端
  11. * @author 小路
  12. */
  13. public class NIOClient {
  14. //通道管理器
  15. private Selector selector;
  16. /**
  17. * 获得一个Socket通道,并对该通道做一些初始化的工作
  18. * @param ip 连接的服务器的ip
  19. * @param port  连接的服务器的端口号
  20. * @throws IOException
  21. */
  22. public void initClient(String ip,int port) throws IOException {
  23. // 获得一个Socket通道
  24. SocketChannel channel = SocketChannel.open();
  25. // 设置通道为非阻塞
  26. channel.configureBlocking(false);
  27. // 获得一个通道管理器
  28. this.selector = Selector.open();
  29. // 客户端连接服务器,其实方法执行并没有实现连接,需要在listen()方法中调
  30. //用channel.finishConnect();才能完成连接
  31. channel.connect(new InetSocketAddress(ip,port));
  32. //将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件。
  33. channel.register(selector, SelectionKey.OP_CONNECT);
  34. }
  35. /**
  36. * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
  37. * @throws IOException
  38. */
  39. @SuppressWarnings(“unchecked”)
  40. public void listen() throws IOException {
  41. // 轮询访问selector
  42. while (true) {
  43. selector.select();
  44. // 获得selector中选中的项的迭代器
  45. Iterator ite = this.selector.selectedKeys().iterator();
  46. while (ite.hasNext()) {
  47. SelectionKey key = (SelectionKey) ite.next();
  48. // 删除已选的key,以防重复处理
  49. ite.remove();
  50. // 连接事件发生
  51. if (key.isConnectable()) {
  52. SocketChannel channel = (SocketChannel) key
  53. .channel();
  54. // 如果正在连接,则完成连接
  55. if(channel.isConnectionPending()){
  56. channel.finishConnect();
  57. }
  58. // 设置成非阻塞
  59. channel.configureBlocking(false);
  60. //在这里可以给服务端发送信息哦
  61. channel.write(ByteBuffer.wrap(new String(“向服务端发送了一条信息”).getBytes()));
  62. //在和服务端连接成功之后,为了可以接收到服务端的信息,需要给通道设置读的权限。
  63. channel.register(this.selector, SelectionKey.OP_READ);
  64. // 获得了可读的事件
  65. else if (key.isReadable()) {
  66. read(key);
  67. }
  68. }
  69. }
  70. }
  71. /**
  72. * 处理读取服务端发来的信息 的事件
  73. * @param key
  74. * @throws IOException
  75. */
  76. public void read(SelectionKey key) throws IOException{
  77. //和服务端的read方法一样
  78. }
  79. /**
  80. * 启动客户端测试
  81. * @throws IOException
  82. */
  83. public static void main(String[] args) throws IOException {
  84. NIOClient client = new NIOClient();
  85. client.initClient(”localhost”,8000);
  86. client.listen();
  87. }
  88. }
———————“

下一篇将介绍反射作用及原理、泛型相关知识、解析XML的几种方式、java与C++的对比、java1.7和1.8的新特性、设计模式、JNI相关知识,还望多多指导与支持~~

(Java总结篇即将告一段落,后面会开始Android重点内容的总结,算法的总结…)

【参考文章:

Java笔试面试题整理第七波的更多相关文章

  1. Java笔试面试题整理第八波

    转载至:http://blog.csdn.net/shakespeare001/article/details/51388516 作者:山代王(开心阳) 本系列整理Java相关的笔试面试知识点,其他几 ...

  2. Java笔试面试题整理第六波(修正版)

    转载至:http://blog.csdn.net/shakespeare001/article/details/51330745 作者:山代王(开心阳) 本系列整理Java相关的笔试面试知识点,其他几 ...

  3. Java笔试面试题整理第五波

    转载至:http://blog.csdn.net/shakespeare001/article/details/51321498 作者:山代王(开心阳) 本系列整理Java相关的笔试面试知识点,其他几 ...

  4. Java笔试面试题整理第四波

    转载至:http://blog.csdn.net/shakespeare001/article/details/51274685 作者:山代王(开心阳) 本系列整理Java相关的笔试面试知识点,其他几 ...

  5. Java笔试面试题整理第三波

    转载至:http://blog.csdn.net/shakespeare001/article/details/51247785 作者:山代王(开心阳) 本系列整理Java相关的笔试面试知识点,其他几 ...

  6. Java笔试面试题整理第二波

    转载至:http://blog.csdn.net/shakespeare001/article/details/51200163 作者:山代王(开心阳) 本系列整理Java相关的笔试面试知识点,其他几 ...

  7. Java笔试面试题整理第一波

    转载至:http://blog.csdn.net/shakespeare001/article/details/51151650 作者:山代王(开心阳) 本系列整理Java相关的笔试面试知识点,其他几 ...

  8. Java工程师笔试题整理[校招篇]

    Java工程师笔试题整理[校招篇]     隔着两个月即将开始校招了.你是不是也想借着这个机会崭露头角,拿到某些大厂的offer,赢取白富美.走上人生巅峰?当然如果你还没能打下Java基础,一定要先打 ...

  9. Java笔试面试题007

    Java笔试面试题007 1.请用正則表達式匹配出QQ号(如果QQ号码为5-10位). 解答: ^ \d{5,10}$ 2.String, StringBuffer StringBuilder的差别. ...

随机推荐

  1. Win10系列:C#应用控件基础23

    Telerik UI Controls for Windows 8 Telerik UI Controls for Windows 8是一套为创建Windows UWP应用而设计的工具集,开发人员可以 ...

  2. learning makefile automatic dependency generation

  3. Linux 搭建Hadoop集群 ----workcount案例

    在 Linux搭建集群---JDK配置 Linux搭建集群---SSH免密登陆 Linux搭建集群---集群搭建成功 的基础上实现workcount案例 注意 虚拟机三台启动集群(自己亲自搭建) 1. ...

  4. 防Bug笔记

    最近不知道咋回事,写代码特别多bug出来.故专门做一笔记,记下容易犯的bug,以期日后减少低级错误. Python 1. JavaScript 1.多用``, 少用''/""

  5. 指导手册05:MapReduce编程入门

    指导手册05:MapReduce编程入门   Part 1:使用Eclipse创建MapReduce工程 操作系统: Centos 6.8, hadoop 2.6.4 情景描述: 因为Hadoop本身 ...

  6. mysql错误集合

    一.This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA in its de 错误解决办法 这是我们开启了bin-log ...

  7. [Spring Boot]什么是Spring Boot

    <Spring Boot是什么> Spring Boot不是一个框架 是一种用来轻松创建具有最小或零配置的独立应用程序的方式 用来开发基于Spring的应用,但只需非常少的配置. 它提供了 ...

  8. CSS旋转缩放

    <style type="text/css"> figure{ float: left;}.test1{ border-radius: 0px; height: 200 ...

  9. 学习Hibenate随笔

    1.Hibenate是一个开放源代码的对象关系映射框架,它对JDBC进行了轻量级的对象封装,将pojo类与数据库表建立映射关系,是一个全自动orm框架,Hibenate可以自动生成sql语句,自动执行 ...

  10. MySQL数据库内置加密函数总结

    首先,我认识的加密函数有以下几个: password(plainText):旧版(OLD_PASSWORD())加密后长度16位,新版41位select length(password("1 ...