目录
简介
可以将一个类的定义放在另一个类的定义内部,这就是内部类。
内部类是一种非常有用的特性,因为它允许你把一些逻辑相关的类组织在一起,并控制位于内部的类的可视性。
最初,内部内看起来就像是一种代码隐藏机制:将类置于其他类的内部,但,内部内的作用远不止如此,它了解外围类,并能与之通信;并且使用内部内一般会使代码更加清晰。
下面我们来了解下内部类:
创建内部类
首先看如下代码
//外部类不能直接访问非静态内部类的成员(形成很好的封装)
//非静态内部类不能有静态方法,静态属性,静态初始化块
public class TestInnerClass {
public static void main(String[]args) {
//创建内部类对象
Outer.Inner inner=new Outer().new Inner();
inner.show();
}
}
class Outer{
private int age= 10;
public void testOuter() {
System.out.println("");
}
class Inner{
//定义内部类好处
//可以直接访问外部类的成员
int age=20;
public void show(){
System.out.println("w外部age="+Outer.this.age);
System.out.println("wne内部age="+this.age);
}
}
}
可看出这是Inner是定义在Outer的内部,而你想从外部类的非静态方法之外的的任意位置创建某个内部类的对象,那么必须像在main()方法中那样,具体地指名这个对象的类型:OuterClassName.InnerClassName。
链接到外部类
到这里 我们是知道了怎么创建内部类(无非是在写在一个类里面),和内部类对象的创建方式,我们也许只了解到内部类只是一种名字隐藏和组织代码的模式。这很有用但是还有其他作用:当生成一个内部类的对象时,此对象与制造它的外围对象之间就有了一种联系,所以它能访问其外围对象的所以成员,而不需要任何特殊条件,此外,内部类还有其他外围类的所有元素的访问权。
下面举个例(只有举例才能更方便的明白,虽然代码多,但是很简单)
//接口
interface Selector {
boolean end();
void next();
Object current();
}
public class Sequence {
private Object[] items;
private int next = 0;
public Sequence(int num) {
items = new Object[num];
}
public void add(Object item) {
if(next<items.length){
items[next++] = item;
}
}
private class SequenceSelector implements Selector {
private int i = 0;
@Override
public boolean end() {
return items.length == i;
}
@Override
public void next() {
if (items.length > i) {
i++;
}
}
@Override
public Object current() {
return items[i];
}
}
public Selector getSelector() {
return new SequenceSelector();
}
public static void main(String[] args) {
Sequence sequence = new Sequence(10);
for (int i = 0; i < 10; i++) {
sequence.add(i);
}
Selector selector = sequence.getSelector();
while (!selector.end()) {
System.out.println(selector.current());
selector.next();
}
}
}
//output
//0 1 2 3 4 5 6 7 8 9
这里我解释下该代码
首先main()中创建Sequence对象,并调用add()方法添加了0,1,2,3…7…9
这里看一下SequenceSelector内部类,首先SequenceSelector是提供Selector功能的private类。可以看见其中end(),current(),next()方法都用到objects(也就是上面代码的items),当然这里只是引用,并非SequenceSelector的一部分,而是外部类中的private字段。这里就说明了内部类可以随意访问外部类,尽管外部类的字段是private字段。
然后创建SequenceSelector对象并向上转型为Selector ,然后while循环输出items
所以内部类自动拥有外部类的所以成员的访问。这是如何做到的?
当外部类的对象创建了一个内部类对象时候,此内部类对象必定会秘密地捕获一个指向那个外部类对象的引用。然后在你访问外部类成员时,就是那个引用来选择外部类中的成员。这里编译器会帮你处理这些细节:这里你可以看见内部类对象只能在与其外部类的对象相关联的时候才会被创建。
构造内部类的时候,需要一个指向其外部类对象的引用,如果编译器访问不到这个引用就会报错,这里绝大时候你无需担心,因为这属于编译器管。
使用.this与.new
前面我们讲了生成内部类对象,这里如果你需要生成外部类对象的引用,可以使用外部类的名字后面加“.this”
下面代码很好的解释了如何调用外部类对象
public class DotThis {
void f(){
System.out.println("DotThis.f()");
}
public class Inner{
public DotThis outer(){
return DotThis.this;
}
}
public Inner inner(){
return new Inner();
}
public static void main(String[] args) {
DotThis dt = new DotThis();
DotThis.Inner dti = dt.inner();
dti.outer();
}
}
//Output:
//DotThis.f();
有时候你可能想告知某些对象,去创建其某个内部类对象。要实现此目的你需要使用“.new”语法
代码如下:
public class DotNew {
public class Inner{}
public static void main(String[] args) {
DotNew dn = new DotNew();
DotNew.Inner dni = dn.new Inner();
}
}
这里如果你想直接创建内部类对象,而不是像上一个代码一样通过Inner()方法返回对象,这时候就必须得使用.new像上面的代码一样创建内部类对象。
所以要获得内部类对象必须得先拥有外部类对象,这是因为内部类对象会暗暗的链接到创建它的外部类对象上。
内部类与向上转型
当将内部类向上转型为基类,尤其是转型为一个接口的时候,内部类就有了用武之地。
所得到只是指向基类或者接口的引用,所以能够很好的隐藏实现细节。
看下面代码
//现在Contents和Destination表示客户端程序员可用的接口。
//(记住,接口的所有成员自动被设置为public的。)
public interface Contents {
int value();
}
public interface Destination {
String readLabel();
}
public class Parcel4 {
private class PContents implements Contents{
@Override
public int value() {
return 0;
}
}
protected class PDestination implements Destination{
private String label;
private PDestination(String whereTo) {
label = whereTo;
}
@Override
public String readLabel() {
return label;
}
}
public Destination destination(String s) {
return new PDestination(s);
}
public Contents contents() {
return new PContents();
}
}
class TestParcel{
public static void main(String[] args) {
Parcel4 p = new Parcel4();
//向上转型
Contents c = p.contents();
//向上转型
Destination d = p.destination("Tasmania");
//这里不能将外部类转型为内部类,因为内部类为private
//! Parcel4.PContents pc = p.new PContents();
}
}
这里可以看见PContents是private,所以除了Parcel4,没人能访问它,而PDestination 是protected,所以只能由Parcel4的子类,与Parcel4同一个包中类访问。这意味着客户端人员想访问或了解这些成员,是受限制的。
还有,不能向下转型为private内部类(或protected内部内,除非是继承自它的子类),因为不能访问其名字,就像TestParcel中例子。
于是private内部类给类的设计者提供了一种途径,通过这种方式可以完全阻止任何依赖于类型的编码,并且完全隐藏了实现的细节
此外从客户端成员角度来讲,由于不能访问任何新增加的,原本不属于公共接口的方法,所以扩展接口是没价值的。
在方法和作用域内的内部类(局部内部类)
下面一个代码展示了在方法的作用域内创建一个类。这叫做局部内部类
// 局部内部类
public class Parcel5{
public Destination destination(String s){
class PDestination implements Destination{
private String label;
private PDestination(String whereTo){
label = whereTo;
}
public String readLabel(){
return label;
}
}
return new PDestination(s);
}
public static void main(String[] args){
Parcel5 p = new Parcel5();
Destination d = p.destination("Tasmania");
System.out.println(d.readLabel());
}
}
PDestination是方法destiantion()方法的一部分,而不是类Parcel5的一部分,所以在方法destination()之外是不能访问PDestination。
注意这里return返回的是PDestianation类型(向上转型),而destiantion()的返回值是Destination ,这里PDestianation转为Destination 类型,所以这里Destination 是PDestianation的基类。
注意:这里不意味着destiantion方法结束完毕后,PDestianation就不可用了
下面展示在任意作用域中嵌入一个内部类
public class Parcel6{
private void internalTracking(boolean b){
if(b){
class TrackingShip{
private String id;
TrackingShip(String s){
id = s;
}
String getShip(){
return id;
}
}
//这里可以继续定义,虽然离开TrackingShip类的范围
//但是还在if作用域中,这里大家应该有点了解的感觉了
TrackingShip ts = new TrackingShip("slip");
String s = ts.getShip();
}
//这里已经超过if语句快的范围,再使用TrackingShip类就不行了
}
public void track(){
internalTracking(true);
}
public static void main(String[] args){
Parcel6 p = new Parcel6();
p.track();
}
}
TrackingShip类被嵌入在if语句的作用域内,它和其他类一起被编译过了。然而,在定义内部类TrackingShip的作用域之外(定义TrackingShip的作用域的也就是if{}),它是不可用的。除此之外,它与普通类一样。
匿名内部类
了解匿名内部类之前先看看下面一个例子
//匿名内部类
interface Contents{
int value();
}
public class Parcel7{
public Contents contents(){
return new Contents(){
private int i;
public int value(){
return i;
}
};//这里需要有个分号
}
public static void main(String[] args){
Parcel7 p = new Parcel7();
Contents c = p.contents();
}
}
大家肯定会发现Contents()方法很奇怪,大家之前都没见过,而这种的写法正是匿名内部类的写法
看起来是你想创建一个Contents对象,但是在这时候你又说“等等,我还行在其中插入一点定义。”
上述匿名内部类的语法其实是下面的代码的简化
public class Parcel7b{
class MyContents implements Contents{
private int i;
//方法value
public int value(){
return i;
}
}
public Contents contents(){
return new MyContents();
}
public static void main(String[] args){
Parcel7b p = new Parcel7b();
Contents c = p.contents();
}
}
这个匿名内部类使用了默认构造器来生成Contents 。下面代码展示的是如果你的基类需要一个有参数的构造器:
//匿名内部类带参
public class Parcel8{
public Wrapping wrapping(int x){
return new Wrapping(x){
public int value(){
return super.value()*47;
}
};
}
}
这里很好理解,只需要传参就行,这里的super.value()*47;实际上调用的是一下的代码
public class Wrapping{
private int i;
public Wrapping(int x){
i = x;
}
public int value(){
return i;
}
}
注意一下:上面大家可能注意到了匿名内部类}结构后会有“;”,这里并不是用来标记此内部类结束的,实际上,它标记的是表达式的结束,只不过这个表达式正巧包含了匿名内部类。因此这个分号是和其他地方使用的效果一样。
在匿名类中定义字段时,还能够对其执行初始化操作:
下面是简单的给一个字段赋值
// 初始化匿名内部类的字段
public class Parcel9{
public Destination destination(final String dest){
return new Destination(){
private String label = dest;
public String readLabel(){
return label;
}
}
}
public static void main(String[] args){
Parcel9 p = new Parcel9();
Destination d = p.destination("Tamania");
}
}
如果定义一个匿名内部类,并希望它使用外部类定义的对象,那么编译器会要求参数是final,不然编译器会报错
上面是简单的给一个字段赋值,那么此例中方法是很好的,但要做一些类似构造器的行为,该怎么办?
在匿名内部类中不能命名构造器(因为它根本没有名字),但通过实例初始化,就能够达到为匿名内部类创建一个构造器的效果:如下代码
// 为匿名内部类实现一个构造方法初始化
abstract class Base{
public Base(int i){
System.out.println("Base constructor , i=" + i);
}
public abstract void f();
}
public class AnonymousConstructor{
public static Base getBase(int i){
return new Base(i){
{
System.out.println("Inside instance initializer");
}
public void f(){
System.out.println("In anonumous f()");
}
};
}
public static void main(String[] args){
Base base = getBase(43);
base.f();
}
}
//Output:
//Base constructor , i=47
//Inside instance initializer
//In anonumous f()
在此例中变量i不一定要求是final。因为i被传递给了匿名内部类的基类(也就是上面的abstract class Base)的构造器,它并不会在匿名内部类中直接使用。
再看一个例子
// 实例初始化
public class Parcel10{
//两个形式参数都是在内部类里面直接使用的,需要final修饰
public Destination destination(final String,final float price){
return new Destination(){
private int cost;
//实例初始化,相当于构造器
{
cost = Math.round(price);
if(cost>100){
System.out.println("Over budget!");
}
}
private String label = dest;
public String readLabel(){
return label;
}
};
}
public static void main(String[] args){
Parcel10 p = new Parcel10();
Destination d = p.destination("Tasmania",101.395F);
}
}
//Output:
//Over budget!
在实例初始化的内部(也就是{}的内部),可以看到有一段代码,它们不能作为字段初始化动作的一部分来执行(就是if语句)。所以对于匿名内部类而言,实例初始化的实际效果就是构造器,当然,它也受了限制,你不能重载实例初始化方法,所以仅有一个接口(也就是只能有一个{ })。
嵌套类
如果不需要内部类对象与其外围类对象之间又关系,那么可以将内部类声明为static。这通常称为嵌套类。
之前我们说过普通内部类对象隐式的保存了一个引用,指向创建它的外部类。但是嵌套类有所不同:
嵌套类的含义:
- 要创建嵌套类对象,并不需要其外部类的对象。
- 不能从嵌套类的对象中访问非静态的外部类对象。
注意:嵌套类和普通内部类还有个区别。普通内部类的字段和方法,只能放在类的外部层次上,所以普通内部类不能又static数据和static字段,也不能含有嵌套类。但是嵌套类可以包含所有这些东西。
public class Parcel11{
//嵌套类---用static修饰内部类
private static class ParcelContents implements Contents{
private int 1 = 11;
public int value(){
return i;
}
}
protected static class ParcelDestiantion implements Destination{
private String label;
private ParcelDestination(String whereTo){
label = whereTo;
}
public String readLabel(){
return label;
}
//嵌套类可以拥有static元素
public static void f(){}
static int x = 10;
static class AnotherLevel{
public static void f(){}
static int x = 10;
}
}
public static Destination destination(String s){
return new ParcelDestination(s);
}
public static Contents contents(){
return new ParcelContents();
}
public static void main(String[] args){
Contents c = contents();
Destination d = destination("Tasmania");
}
}
可以看到上面代码在main()方法中,没有任何Parecl11的对象是必需的;而是使用选取static成员的普通语法来调用方法:这些方法返回Contents 和Destination 的引用。
就像前面看到的那样,在一个普通内部类中,通过一个特殊的this引用可以链接到其外部类对象。嵌套类就没有这个特殊的this引用,这使得它类似一个static方法。
接口内部的类
正常情况下,不能在接口内部放置任何代码,但嵌套类可以作为接口的一部分。你放到接口中的任何类都是public static的。因为类是static的,只是将嵌套类置于接口的命名空间欸,并不违反接口的规则。你甚至可以在内部类中实现外部接口,如下代码:
// 在接口中实现嵌套类并实现外部接口
public interface ClassInInterface{
void howdy();
//类Test默认是public static的
//在这里,内部类直接实现了外部接口ClassInInterface
class Test implements ClassInInterface{
public void howdy(){
System.out.println("Howdy!");
}
public static void main(String[] args){
new Test().howdy();
}
}
}
//Output
//Howdy!
如果你想要创建某些代码,使他们可以被某个接口的所有不同实现所共用。那么使用接口中嵌套类就会很方便。
从多层嵌套类中访问外部类的成员
一个内部类被嵌套多少层并不重要:它能透明地访问所有它所嵌入的外围类的所有成员:
// 从多层嵌套类中访问外部类的成员
class MNA{
private void f(){}
class A{
private void g(){}
public class B{
void h(){
g();
f();
}
}
}
}
public class MultiNestingAccess{
public static void main(String[] args){
MNA mna = new MNA();
MNA.A mnaa = mna.new A();
MNA.A.B mnaab = mnaa.new B();
mnaab.h();
}
}
可以看得出得到B对象后,可以随意的访问它外部类的所有成员,方法,即使是private
为什么需要内部类
之前说了这么多,从最基本的内部类创建,局部内部类,匿名内部类,嵌套类等。那么这些东西到底有什么含义,使用它们有什么用?
下面进行解释:
一般来讲,内部类继承自某个类或实现某个接口;内部类的代码操作创建它的外部类的对象。所有:
内部类提供了某种进入其外部类的窗口。
这里提个问题:内部类实现接口和外部类实现接口的区别?
答案是:后者不总是能享受到接口的方便,有些时候需要用到接口的实现,对于内部内则没有这个影响,所有内部类使用的原因之一是:每个内部类都能独立地继承自一个(接口的)实现,所有无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。
内部类使得多重继承的解决方案变得完整:内部类允许继承多个非接口类型(类或者抽象类)
为了看到更多细节,让我们考虑这样一种情况:即必须在一个类中以某种方式实现两个接口。
由于接口的灵活性;这里有两个选择:
- 使用单一类
- 使用内部类
看下面代码
// 在一个类中以某种方式实现两个接口
interface A{}
interface B{}
//单一类
class X implements A,B{}
//使用内部类
class Y implements A{
B makeB(){
//匿名内部类
return new B(){};
}
}
public class MultiInterface{
static void takesA(A a){}
static void takesB(B a){}
public static void main(String[] args){
X x = new X();
Y y = new Y();
takesA(x);
takesA(x);
takesB(y);
//y.makeB()的返回值是B类型,这里显示了内部类的使用灵活性
//以及独立性(独立继承一个接口)
takesB(y.makeB());
}
}
看了上面的代码,大家会发现内部类好像还是有点用。比如Y类没有继承B接口也就以为这它可以不实现B接口中的方法,这不就灵活起来了吗。
上面使用单一类或者内部类还是得分情况。
如果拥有的是抽象的类或者具体的类,而不是接口,那就之一使用内部类才能实现多继承。
// 内部类实现真正意义上的的多继承
class D{}
abstract class E{}
//这不就相当于多继承了吗
class Z extends D{
E makeE(){
return new E(){};
}
}
public class MultiImplementation{
static void takesD(D a){}
static void takesE(E e){}
public static void main(String[] args){
Z z = new Z();
takesD(z);
takesE(z.makeE());
}
}
使用内部类还可以获得其他一些特性:
- 内部类可以有多个对象实例,每个实例都有自己的状态信息,并且与其外部类对象的信息相互独立。
- 在单个外部类中,可以让多个内部类以不同的方式实现同一个接口,或继承同一个类。在后续的博文中会给出代码示例。
- 创建内部类对象的时刻并不依赖于外部类对象的创建。
- 内部类并没有令人迷惑的"is-a"的关系,它就是一个独立的实体。
内部类的继承
因为内部类的构造器必须链接到指向外部类对象的引用,所有在继承内部类的时候,问题会比较复杂。
问题在于,因为外部类的对象一定在内部类创建之前被创建,内部类指向外部类的那个“隐藏”引用必须被初始化。
// 内部类的继承
class WithInner{
class Inner{}
}
//继承内部类
public class InheritInner extends WithInner.Inner{
//在这里必须这么写,默认的构造器没用.
//构造器中传入一个所继承的内部类的外部类对象.
InheritInner(WithInner wi){
//必须这么写,固定句式enclosingClassReference.super();
wi.super();
}
public static void main(String[] args){
WithInner wi = new WithInner();
InheritInner ii = new InheritInner(wi);
}
}
可以看到,InheritInner 只继承内部类。当要生成一个构造器时,默认的构造器并不算好,而且不能只是传递一个指向外部类对象的引用。此外,必须在构造器内使用如下语法:
enclosingClassReference.super();
这样才提供了必要的引用,编译器才不会报错。
内部类可以被覆盖吗
如果创建了一个内部类,然后继承其外部类并重新定义此内部类时,会发生覆盖吗?
// 覆盖内部类
class Egg{
private Yolk y;
protected class Yolk{
public Yolk(){
System.out.println("Egg.Yolk()");
}
}
public Egg(){
System.out.println("New Egg()");
y = new Yolk();
}
}
public class BigEgg extends Egg{
public class Yolk{
public Yolk(){
System.out.println("BigEgg.Yolk()");
}
}
public static void main(String[] args){
new BigEgg();
}
}
//Output
//New Egg()
//BigEgg.Yolk()
可以看出并没有覆盖,这说明这两个内部类是两个独立的个体。各自在自己的命名空间中。
当我们明确的继承某个内部类:
// 明确继承一个内部类
//外部类Egg2
class Egg2{
protected class Yolk{
public Yolk(){
System.out.println("Egg2.Yolk()");
}
public void f(){
System.out.println("Egg2.Yolk().f()");
}
}
private Yolk y = new Yolk();
public Egg2(){
System.out.println("New Egg2()");
}
public void insertYolk(Yolk yy){
y = yy;
}
public void g(){
y.f();
}
}
public class BigEgg2 extends Egg2{
public class Yolk extends Egg.Yolk{
public Yolk(){
System.out.println("BigEgg2.Yolk()");
}
public void f(){
System.out.println("BigEgg2.Yolk().f()");
}
}
public BigEgg2(){
insertYolk(new Yolk());
}
public static void main(String[] args){
Egg2 e2 = new BigEgg2();
e2.g();
}
}
//Output
//Egg2.Yolk()
//New Egg2()
//Egg2.Yolk()
//BigEgg2.Yolk()
//BigEgg2.Yolk().f()
这是有些小伙伴看到这里有些蒙蔽了,LZ刚看的时候也很懵逼
- 首先第一个输出的是Egg2.Yolk(),创建new BigEgg2();对象先去指向基类构造器,而调用构造器之前会先指向private Yolk y = new Yolk();所有就有了Egg2.Yolk(),然后是New Egg2()
- 为啥后面又有Egg2.Yolk(),在调用insertYolk()的时候,new Yolk()先走的流程是BigEgg2的Yolk,因为这里也明确的继承了内部类,所有先初始化基类的构造器。
- 最后输出的BigEgg2.Yolk().f()说明的确被覆盖了
局部内部类和匿名内部类的比较
首先看下面代码
// 局部内部类和匿名内部类的比较
interface Counter{
int next();
}
public class LocalInnerClass{
private int count = 0;
Counter getCounter(final String name){
class LocalCounter implements Counter{
public LocalCounter(){
System.out.println("LocalCounter()");
}
public int next(){
//打印变量name的值
System.out.println(name);
//返回表达式count++的值
return count++;
}
}
return new LocalCounter();
}
Counter getCounter2(final String name){
return new Counter(){
{
System.out.println("Counter()");
}
public int next(){
System.out.println(name);
return count++;
}
};
}
public static void main(String[] args){
LocalInnerClass lic = new LocalInnerClass();
Counter c1 = lic.getCounter("Local inner"),
c2 = lic.getCounter2("Anonymous inner");
for(int i = 0;i<5;i++){
System.out.println(c1.next());
}
for(int i = 0;i<5;i++){
System.out.println(c2.next());
}
}
}
//Output
//LocalCounter()
//Counter()
//Local inner 0
//Local inner 1
//Local inner 2
//Local inner 3
//Local inner 4
//Anonymous inner 5
//Anonymous inner 6
//Anonymous inner 7
//Anonymous inner 8
//Anonymous inner 9
这里用局部内部类和匿名内部类同时实现相同的功能
既然局部内部类的名字在方法外是不可见的,那为什么仍然使用局部内部类呢?唯一的理由是:我们需要一个命名的构造方法(匿名内部类没有命名的构造方法),或者重载构造方法,而匿名内部类只能用于实例初始化
内部类标识符
由于每个类都会产生一个.class文件,其中包含了如何创建该类型的对象的全部信息(此信息包含Class对象),这里内部类也必须生成一个.class文件以及包含它的Class对象信息。
这些类文件的命名有严格的规则:外部类的名字,加载“$” ,在加上内部类名字。例如:LocalInnerClass.java生成的.class文件包括:
Counter.class
LocalInnerClass$1.class
LocalInnerClass$1LocalCounter.class
LocalInnerClass.class
如果内部类是匿名的,编译器会简单的产生一个数字作为其标识符,如果内部类是嵌套在别的内部类之中,只需直接将它们的名字加在其外部类标识符与" $ "的后面。
虽然这种命名格式简单而直接,但它还是很健壮的,足以应对绝大多数情况。因为这是Java的标准命名方式,所以产生的文件自动都是平台无关的。
总结
这里基本上讲讲完了内部类基本知识,但是我任务绝大多数人还是懵懵懂懂(包括我自己),但是随着时间的推移,读者将能够更好的识别何时使用接口何时使用内部类。