Java内部类


Java内部类

内部类(inner class)是定义在另一个类中的类。使用内部类的原因主要有三个:

  • 内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据。
  • 内部类可以对同一个包中的其他类隐藏起来
  • 当想要定义一个回调函数且不想编写大量代码的时候,可以使用匿名内部类比较方便。

内部类按照不同的应用形式和需求场景分为四种,我在这里将其称之为普通内部类局部内部类匿名内部类静态内部类

内部类的语法比较复杂。内部类是一种编译器现象,在编译过程中,编译器会为其做不少的工作。比如编译器会把内部类翻译成用$分隔外部类名与内部类名的常规类文件,但是jvm对此一无所知。在局部内部类中编译器会为每一个由外部传入的局部变量建立相应的数据域。


普通内部类 使用内部类访问对象状态

在下面的示例代码中,将会体会内部类如何访问外部类对象的状态,此代码实现一个时钟效果,每秒钟会产生一个通告并且鸣出一声

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class TalkingClock{
private int interval;
private boolean beep;
public TalkingClock(int interval, boolean beep){
this.interval = interval;
this.beep = beep;
}
public void start(){
ActionListener listener = new TimePrinter();
Timer t = new Timer(interval,listener);
t.start();
}
public class TimePrinter implements ActionListener{
@Override
public void actionPerformed(ActionEvent e) {
Date now = new Date();
System.out.println("At the tone , the time is "+ now);
if(beep)
Toolkit.getDefaultToolkit().beep();
}
}
}

在外围类的作用域之外,可以这样引用内部类 : OuterClass.InnerClass
eg:

1
2
TalkingClock job = new TalkingClock(1000,true);
TalkingClock.TimePrinter listener = job.new TimePrinter();

在对文件进行编译的过程中,会生成TalkingClock$TimePrinter.class类文件。通过反射技术可以得到如下的大致内容是

1
2
3
4
5
public class TalkingClock$TimePrinter{
public TalkingClock$TimePrinter(TalkingClock);
final TalkingClock this$0;
public void actionPerformed(java.awt.event.ActionEvent);
}

编译器为了引用外围类,生成了一个附加的实例域this$0 (名字this$0是由编译器合成的,在自己的代码中不能够引用它)。另外还可以看到构造器的TalkingClock参数。

在普通的内部类中,除了其外部类知道它的存在外,对其他的包级类都隐藏了起来。


局部内部类

在上面TalkingClock代码中可以发现,内部类TimePrinter这个类名字只在start方法里创建这个类型对象时使用了一次。当遇到这种情况的时候,可以在一个方法里面定义局部内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void start(){
class TimePrinter implements ActionListener{
@Override
public void actionPerformed(ActionEvent e) {
Date now = new Date();
System.out.println("At the tone , the time is "+ now);
if(beep)
Toolkit.getDefaultToolkit().beep();
}
}
ActionListener listener = new TimePrinter();
Timer t = new Timer(interval,listener);
t.start();
}

局部内部类不能使用public或private访问说明符进行声明。它的作用域被限制在这个声明这个局部内部类的块中。

局部内部类有一个优势,即对外完全隐藏。除了start方法之外,没有任何方法知道TimePrinter类的存在,TalkingClock类也不知道。

和其他内部类相比,局部内部类还有一个优点,他们不仅能够访问包含他们的内部类,还可以访问局部变量。不过这些局部变量需要被声明为final。下例将给出说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void start(int interval ,final boolean beep){
class TimePrinter implements ActionListener{
@Override
public void actionPerformed(ActionEvent e) {
Date now = new Date();
System.out.println("At the tone , the time is "+ now);
if(beep)
Toolkit.getDefaultToolkit().beep();
}
}
ActionListener listener = new TimePrinter();
Timer t = new Timer(interval,listener);
t.start();
}

在此。我们考察一下start方法的控制流程

  • 调用start方法
  • 调用内部类TimePrinter的构造器,以便初始化对象变量的值
  • 将listener引用传递给Timer构造器,定时器开始计时,start方法结束。此时,start方法的beep参数不复存在。
  • 然后,actionPerformed方法执行if(beep)…。

为了让actionPerformed方法工作,TImePrinter类在beep域释放之前将beep域start方法的局部变量进行备份。
反编译一下TalkingClock$TimePrinter类会得到下述结果:

1
2
3
4
5
6
public class TalkingClock$TimePrinter{
public TalkingClock$TimePrinter(TalkingClock,boolean);
final TalkingClock this$0;
final boolean val$beep;
public void actionPerformed(java.awt.event.ActionEvent);
}

注意构造器的boolean参数和val$beep实例变量。当创建一个对象的时候,beep就会被传递给构造器,并储存在val$beep这个final域里面。在start方法里面将beep参数声明成final,对它进行初始化后不能够在此修改。因此,就使得局部变量和局部类内建立的拷贝保持一致。


匿名内部类

将局部内部类再往下深入一步,假如只创建这个类的一个对象,就不需要命名了。此时类被称为匿名内部类。
匿名内部类的语法如下所示:

new SuperType(construcation parameters){


innner class method and data


}

例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void start(int interval ,final boolean beep){
ActionListener listener = new ActionListener(){
@Override
public void actionPerformed(ActionEvent e) {
Date now = new Date();
System.out.println("At the tone , the time is "+ now);
if(beep)
Toolkit.getDefaultToolkit().beep();
}
};
Timer t = new Timer(interval,listener);
t.start();
}


静态内部类

有时候使用内部类只是只是为了将类隐藏在另一个类里面,而不需要内部类去引用外围类对象的。此时,可以将内部类声明为static,以取消对外围类的引用。将TimePrinter类中去掉外围类的beep对象变量,就可以将TimePrinter声明为static。

1
2
3
4
5
6
7
8
public static class TimePrinter implements ActionListener{
@Override
public void actionPerformed(ActionEvent e) {
Date now = new Date();
System.out.println("At the tone , the time is "+ now);
Toolkit.getDefaultToolkit().beep();
}
}

注意:只有内部类可以声明为static && 声明在接口中的内部类自动成为static和public类

Java中字符集的处理


设计Java历史背景以及其特点

Java的设计之初是为了用于像有线电视转换盒一样的消费设备。由于这类设备的处理能力和内存都很有限,所以语言必须要非常小并且能够生成非常紧凑的代码。另外,由于不同的厂商会选择不同的CPU(指令集会不一致),所以这种语言需要设计成不能与任何特定的体系结构捆绑在一起。

Java的设计模型源于pascal的发明者Niklaus Wirth 设计出的一种为假想的机器生成中间代码的可移植性语言。(假想的机器被称为虚拟机。Java虚拟机JVM的命名由此而来)

Java设计出来的数据类型都是固定存储大小。比如,int为32位,short为16位。这样设计的原因Java程序必须要保证在所有机器上都能够得到相同的运算结果。所以每一种数据类型的取值范围必须固定。

与此相比,C或者C++需要对不同的指令集(CPU)选择最为有效的的整形,这样就有可能造成一个在32位处理器上运行很好但是在16位系统上运行却发生整数溢出。


Java的char类型以及Unicode编码表的由来

在Java里面有8种基础类型,4种整型,2中浮点类型,1种用于表示真值的boolean类型,一种用于表示Unicode的字符单元的字符类型char类型。

char类型用于表示单个字符。通常用来表示字符常量。例如‘A’编码为65所对应的字符常量。又例如public static void main(String\u005B\u005D args)也是符合语法规则。

要想弄清楚char,就应该需要了解Unicode编码表。

Unicode打破了传统字符编码方法的限制。在Unicode出现之前,已经有了很多不同的标准:美国的ASCII、西欧语言中的ISO8859-1、俄国的KOI-8、中国的GB 18030和BIG-5等。这样就产生了两个问题:

  • 对于任意给定的代码值比如56,在不同的编码方案下可能对应不同的字母;
  • 采用大字符集的语言其编码长度有可能不同。例如,有些常用的字符采用单字节编码,而另外一些字符则需要两个或更多个字节。

设计Unicode编码的目的就是为了解决这些问题。在20世纪80年代开启设计工作时,人们认为两个字节的代码宽度足以应对世界上各种语言的所有字符编码,并有足够的空间留给未来扩张。在设计JAVA的时候就决定采用了16位的Unicode字符集。

不过很遗憾的是,Unicode字符超过了65536个,主要原因是增加了大量的汉语,日语等表意文字。现在16位的char类型已经不足以满足所有的Unicode字符的需要了。

Java语言为了需要解决这个问题。char类型用UTF-16编码来描述一个代码单元。

至于为什么char类型采用UTF-16编码能解决这个问题,首先需要解释两个概念:代码点代码单元

从JDK5.0开始,代码点(code point)是指与一个编码表中的某个字符对应的代码值。在Unicode标准中,代码点采用十六进制书写,并加上前缀U+,例如U+0041(即十进制的65)就是字母‘A’的代码点。代码点可以分成17个代码级别,第一个代码级别称为“基本的多语言级别”,代码点从U+0000到U+FFFF,其中包括了经典的Unicode代码,其他的16个附加级别,代码点从U+10000到U+10FFFF。

UTF-16采用不同长度的编码表示所有的Unicode代码点。在“基本的多语言级别”中,每个字符用16位表示,通常被称为代码单元(code unit)。而辅助字符采用一对连续的代码单元进行编码。这样构成的编码值一定落入基本的多语言级别中空闲的2048字节内,通常被称为替代区域。【U+D800-U+DBFF用于第一个代码单元】,【U+DC00~U+DFFF用于第二个代码单元】。这样设计十分巧妙。我们可以迅速的知道第一个代码单元是一个字符的编码还是一个辅助字符的第一或第二部分。例如代码点为U+1D56B就有两个代码单元U+D835和U+DD6B编码的。

为此,强烈建议不要使用char类型。除非确实需要对UTF-16代码单元进行操作。(原因是char类型变量取到的值仅仅只是16位,当读取的字符属于辅助字符时,16位的char类变量是仅仅只能取到辅助字符的第一个代码单元,会导致乱码)


Unicode编码的三种实现 UTF-16,UTF-32,UTF-8

UTF-16编码是java中字符类型采用的编码。是Unicode编码设计的一种实现。

此外还有UTF-8和UTF-32编码也是Unicode的实现。

其中UTF-16和UTF-32编码是Unicode设计的16位和32位实现。考虑到最初的实现,我们通常说的Unicode编码指的就是UTF-16编码。

UTF-16长度相对固定,只要不处理大于U+200000范围的字符,每个Unicode代码点使用16位即2字节表示,超出部分使用两个UTF-16即4字节表示。

UTF-32长度始终固定,每个Unicode代码点使用32位即4字节表示
由于UTF-16和UTF-32的Unicode实现采用的是相对固定长度来表示一个字符,占用的内存大小相对而言比较大。例如对于字母‘A’而言,本身一个字节8位就可以表示出来,但是采用UTF-16的情况下需要使用两个字节,UTF-32编码情况下需要4个字节才能表示。相对而言内存消耗是比较大的。

UTF-8是一个可变长度的编码方式。它使用1~4个字节表示一个符号。根据符号的代码点而变化字节长度。

UTF-8的编码规则很简单,只有二条:

  • 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。’A’ 0041 U+0041 0100001
  • 对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。
十六进制 二进制
0000 0000-0000 007F 0xxxxxxx
0000 0080-0000 07FF 110xxxxx 10xxxxxx
0000 0800-0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

下面,还是以汉字“严”为例,演示如何实现UTF-8编码。
已知“严”的unicode是4E25(100 1110 00 10 0101),根据上表,可以发现4E25处在第三行的范围内(0000 0800-0000 FFFF),因此“严”的UTF-8编码需要三个字节,即格式是“1110xxxx 10xxxxxx 10xxxxxx”。然后,从“严”的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,“严”的UTF-8编码是“11100100 10111000 10100101”,转换成十六进制就是E4B8A5。