转载至: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. vs 设置自动缩进tab转换成空格

    工具   选项   文本编辑器    如下图  选中插入空格 使用技巧: 按Ctrl+K+F组合键,可以自动进行代码对齐.

  2. 走进JavaScript

    JavaScript的作用:操作HTML元素,响应用户的操作,处理数据: script标签的type或者language可以写也可以不写: script标签防止位置:head结束之前或者body结束之 ...

  3. VMware与Centos系统安装、重置root密码

    VMware与Centos系统安装   今日任务 .Linux发行版的选择 .vmware创建一个虚拟机(centos) .安装配置centos7 .xshell配置连接虚拟机(centos) 选择性 ...

  4. [转] MySql 数据类型

    转自:http://blog.csdn.net/anxpp/article/details/51284106 1.概述 要了解一个数据库,我们也必须了解其支持的数据类型. MySQL支持所有标准的SQ ...

  5. VIM入门

    目录 一.vim介绍 二.vim颜色显示和移动光标 三.vim一般模式下移动光标 四.vim一般模式下复制.剪切和粘贴 五.进入编辑模式 六.vim命令模式 七.vim实践 一.vim介绍 vi是最重 ...

  6. Delphi xe8 FMX StringGrid根据内容自适应列宽。

    Delphi xe8 FMX StringGrid根据内容自适应列宽. 网上的资料比较复杂,而且不是根据字体字号等设置列宽.故自己写了个function来用. function GetColMaxDa ...

  7. windows2008 转 centos7 数据磁盘NTFS无损挂载

    转换时 原win硬盘里作为系统稳盘的硬盘必须重新格式化才能装机 数据盘在安装ntfs-3g可以直接挂载 几个重要命令: #lsblk  //查看硬盘情况 df -T 只可以查看已经挂载的分区和文件系统 ...

  8. 蘑菇街支付架构 PDF 下载

    蘑菇街支付架构 PDF 下载 下载地址:链接:https://pan.baidu.com/s/1ZffetaUhVMOzb9j2PSQJIQ 密码:iays http://www.java1234.c ...

  9. linux ipv6开启的配置文件

    1./etc/sysconfig/network-scripts/ifcfg-eth0 DEVICE=eth0TYPE=EthernetUUID=9d1d6e2a-cfc5-4e60-8f28-b77 ...

  10. 成功的拆开了SELECT里JOIN个SELECT是啥

    SELECT * FROM table JOIN table ON a=b ----------------------- JOIN (SELECT* FROM table JOIN table ON ...