JAVA 笔记
Posted LaterEqualsNever
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JAVA 笔记相关的知识,希望对你有一定的参考价值。
关于interface值得被了解的东西
当我们最初开始接触编程的时候,都会听到类似,你可以使用这个API,使用那个API 类似的说法。
没错,API就是说Application Programming Interface,也就是应用程序编程接口。
但对应到Java里来说,初学乍练之时可能会造成一定的疑惑。因为,大部分时间来说,
你发现在使用的不是别人的工具类(class)里的方法,但是Java里的接口,不是说用interface定义的吗?
要解开这个误区,我们先来看一看所谓的API到底指的是什么?书面定义来说:API是一些预先定义的函数,目的是提供应用程序与开发人员基于
某软件或硬件得以访问一组例程的能力,而又无需访问源码,或理解内部工作机制的细节。
那么就很好理解了。以Java来说,我们开发中离不开使用的就是JDK里的API。
对应来说,我们实际上通常确实就是在使用JDK里提供的一个个类(也就是class)当中的一个个方法(即函数)。
但同时值得注意的是,我们关心的重点在于,去使用它们来协助完成我们需要的功能,但并不会去操心其内部的具体实现。
所以,我们其实可以看到,这和对于API的概念定义是很相符的。
而我们来看一下接口的书面定义:接口泛指实体把自己提供给外界的一种抽象化物(可以为另一实体),
用以由内部操作分离出外部沟通方法,使其能被修改内部而不影响外界其他实体与其交互的方式。
那么,Java当中关键字interface的自身特性就恰好符合这个概念:只提供方法声明,但不提供具体实现。
但是,我们也可以想到interface声明的接口概念,更像是Java中带有专用型的一种产物。
因为通过接口的概念,我们也可以得知,远远不会仅仅如此。就如同下面的代码一样:
public class Demo {
public void f(){
innterMehotd();
}
// 隐藏的内部实现
private void innterMehotd(){
}
}
这段代码中暴露出的公有方法f()也可以被理解为传统意义上的程序接口。
因为我们可以通过改变innterMehotd()的实现细节来改变程序,但并不会影响f()的消费者。
到这里,我们已经不难理解了,以API和Java里的interface来说:
其实二者都符合对于接口的定义。如果说有所不同,可能就是API泛指了更为广义上的接口定义。
而接口的关键,简而概之就是:我提供访问程序的入口,但内部细节无需你关心。
这也是为什么我们后面会说到,针对接口编程能够降低代码耦合度。就是因为:
接口的特性让它能够保证,即使其实现细节被改变,但并不会影响到其消费者,因为你消费的仅仅只是程序入口。
想必到此,我们已经弄清楚了关于接口的概念上的东西,这很有帮助。
接下来,我们就要来好好看看interface这个东西,了解一下那些关于它值得被了解的有趣的细节和知识。
接口常见的使用方式
接口被用来建立类与类之间的协议。
说到协议我们可能就很容易想到类似HTTP、TCP/IP这样的通信协议。没错,制定协议最大的目的就是:
针对于某项多个个体共同参与的事物,让其必须遵循统一的规范。
而协议的书面意思是指两个或两个以上实体为了开展某项活动,经过协商后双方达成的一致意见。
那么我们首先就来看一种,最符合对于“协议”这个名词的概念的解释的使用方式。
OK,举例来说。假设,我们来设计一套关于机车生产的“协议”:
public interface Vehical {
void drive();
void stop();
void turns();
}
就像这里的代码一样,在实际的生产过程中,可能存在着众多不同厂商生产的各种类型的汽车。
但没关系,只要你想要生产汽车,那么请遵循我的接口(协议)!
你怎么样来生产自己认为满意的汽车无所谓,但必须让它至少具备行驶,制动,转弯等功能(这是协议规定的)。
通过接口进行框架搭建
事实上,针对于接口的特性,肯定还有另一些妙用。首先可能就是,你是一个项目的架构师。
于是你通过项目的需求分析,来设计项目。也就是说,你了解这个项目中的各个功能,但并不需要你来为这些功能提供具体实现。
你只需要针对于这个功能设计好实现规范,最后让相关的人员去负责完成。
以一个最基本的例子来说明,现在你搭建的项目中有一个负责购物的类。于是你开始设计:
public interface Shopping {
void buy();
void sale();
}
最终,当项目搭建完毕后,负责消费功能模块的人员就根据该接口去完成具体的功能类的实现。
利用接口实现“回调”
接着,我们可以看到另一种更常见的使用方式:即我们常说的“回调”。
关于回调其实与我们说到的上一种使用方式的原理很类似。因为简单来说:
回调就是针对于某一问题(事物),你需要一个“解决方案”,但该方案可能存在多种方式或者你希望由别人来给出。
这样的表达,我不知道是不是准确。我们仍然通过一个例子,可以更好的进行理解。
在android编程当中,我们经常会用到下面这样的代码:
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick() {
System.out.println("用户点击按钮了");
}
});
这里的View.OnClickListener就是声明于View类当中的一个接口。我们可以思考一下为什么会设计有这样一个接口?
其实很简单,不正是因为android的设计者在设计View类的时候,就可以预见到View通常是具有“可点击性”的。
而当控件被点击时,对于在Android客户端的消费者来说,很自然的就需要得到一个“响应事件”。
但是这个响应事件既然位于客户端,就当然不会是android的设计者在设计时能够明确的。而应当由客户端的开发者根据不同的实际需求去提供的。
所以,没关系,我们就提供一个来处理“响应事件”的接口(规范)。
作为开发者,只需要按照规范去实现,我们就能按你所愿去回调并正确的响应你的事件。
所以我们不用去查看源码也不难想象,View类当中的设计思想肯定是类似于下面这样的:
public class View {
private OnClickListener mOnClickListener;
// 用户点击屏幕就触发该方法
public void onClick() {
// 回调接口,处理点击事件
mOnClickListener.onClick();
}
public void setOnClickListener(OnClickListener listener) {
mOnClickListener = listener;
}
public interface OnClickListener {
void onClick();
}
}
这实际上也是策略模式的一种体现,正如上面的例子中体现的:
我们的某个方法中接受的参数是某一个接口,于是使用者就可以根据自己的需求来实现这个接口;
然后将接口发送给我们提供的方法,从而得到不同的处理策略。
这时,我们再对我们上述的几种接口的使用方式进行总结。然后不难发现:
无论是哪种使用方式,其核心的思想任然源于接口最重要的特点:提供程序访问入口,不关心具体实现。
我们可以看到,在上面的各种例子中,我们所做的工作都是:
- 在接口中定义好那些潜在的消费者需要访问的功能入口;
- 但是该接口的具体细节,则由具体的对象根据实际情况去更好的实现;
对应于上面例子中的具体情况来说,就正如同:
- “汽车生产协议”中的接口,是提供给那些使用该协议生产汽车的厂商。
- “项目架构师”中的接口,是提供给负责实现该功能模块的核心程序员。
“Android当中的事件监听回调”中,接口是提供给负责具体事件响应实现的安卓客户端程序员。
而实际上到了这里,根据我们上面说到的,我们不难总结到一个观点:接口的特性,让它特别适用于“合作“。
为什么这样说,因为编程的工作基本不可能是一个人去完成的,接口的出现,使得我们可以做到:
针对于那些“由他人负责编写的,但却在自己负责的部分可能需要使用到的功能”进行抽离,将这些部分抽离成接口。
大家在自己的代码中针对于该接口编程(就像上面回调的例子中做的那样),在实现合作的同时,却又互不影响。
“interface”小知识
上面,我们说了一些interface常见的使用方式。下面我们来关注一些关于interface自身的小细节。
默认的static final 域
interface里所有域都被自动的隐式的申明为static且final的,也就是说即时你不这么做,系统也会默认的为你添加,正如下面这样:
public interface Test{
int A = 10; // == public static final int A = 10;
}
这是接口自身的规范,但我们要做的,是思考为什么有这样的设计规范。
首先,我们来分析一下被默认的声明为static是为什么?
很简单,我们知道Java当中interface和abstract class是不能被用以构造对象的。
那么,如果接口中的域不是静态而是属于成员的,这显然是自相矛盾,不合理的。
接着,我们已经说过了,接口的出现,就是为了建立类与类之间的协议。
也就是说,它出现的目的就为了定义多个类共同约定,同意遵循的规范。
那么如果其不是final的,也就证明它可以被任何实现了该接口的类所改变,也就达不到“统一规范”的目的了。
以我们上面说到的定义的“机车生产”的例子来说,假设我们在接口中定义了一个字段”public int tyre = 4;”。
则说明协议的制定者希望:遵循该规范生产的机车,轮胎的数量都按照4个的标准进行生产。
那么,针对于我们说分析的结果来说,就对应于:
- 如果该字段不是static的,则每个实现了该接口的类都可以根据构造对象来改变轮胎数量。
如果该字段不是final的,虽然每个实现了该接口的类仍然都会得到统一的轮胎数量。
但这个轮胎数可以被任一的修改,于是可能会得到被修改为“5个”、“3个”等等这样的数量。**默认的public abstract 方法**
闲话就不多说了,同域的声明一样,接口中的方法会自动的被声明为public abstarct。我们要研究的,仍然是为什么这样设计。
被默认设计为abstarct并不难理解,因为我们说了接口中的方法只声明规范,不提供具体实现。
而为什么是public就很有意思了。网上有一种说法是这样的,之所以这样设计是因为:
接口本身就是要被类所实现才有意义,所以如果它的方法不公有化,就没有意义了。
我个人是这样理解的,这种说法本身是有道理,但却不完全准确。为什么呢?
因为interface也和class一样,它也可以被定义为“包内私有”。也就是说,也可能存在所谓的“包内接口”。
那么想象这样一种情况:针对于包内的接口,肯定只有定义在同一包目录的类能够去实现该接口。
而如果我们将实现接口的类中的方法同样全部定义为包访问权限;也就是说,该接口的适用范围被严格控制在同一包目录下。
从而达到的效果是:类里从接口中实现而来的方法与接口中定义的方法,同样是只能够被同一包中的其它类访问。
那么,接口内的方法如果使用“包权限”访问修饰符,很明显也是行的通的。
但实际上,将接口的使用权限仅仅控制在同一包下,这和接口被设计出来的使用目的显然是相悖的。
因为位于同一包目录下的类关联已经足够密切,通常它们都是由同一个人或者同属的一群人来负责编写的。
而实际上,就像我们在之前说到的“Android中事件监听响应事件”中用到的接口一样。
该接口的出现,是Android系统的设计者们提供给那些,Android客户端程序员在将来使用的。
那么将接口的使用范围仅仅控制在包权限之中,显然就是远远不够的。所以将接口内的方法默认为public就很合理了。
这样的原理实际上很显而易见,但我们仍然通过一个最基础的例子来更好的进行理解:
首先,我们在包目录“a”里,定义了一个interface:
package com.tsr.a;
public interface Interface {
void f();
}
接着,我们定义了一个类来实现该接口:
package com.tsr.a;
public class ClassA implements Interface{
@Override
public void f() {
System.out.println("fffff");
}
}
下面就是关键了,假设我们在包目录B下的一个类里针对该接口进行了编程:
package com.tsr.b;
import com.tsr.a.ClassA;
import com.tsr.a.Interface;
public class Test {
public static void main(String[] args) {
Test test = new Test();
test.useInterface(new ClassA());
}
void useInterface(Interface in){
in.f(); //想象一下,如果方法不为public,这里能访问到吗?
}
}
结果就正如我们在程序注释里说的,如果接口的方法不是默认的被强制为public。
那么可能这个方法根本就很难被得以使用,从而当然也就失去了接口的意义。
Java提供权限访问修饰符,是出于面向对象的“封装性”的考虑。
因为对于类来说,我们应该选择去隐藏那些我们不希望提供给别人的部分。
但对于接口来说,其出现的本来目的就是对使用该接口的类声明用于声明一种公有的规范。
那么,在公有的规范里,却又希望一些东西不能被规范的遵守者察觉或使用,那么不是很矛盾吗?
针对于接口进行解耦
就正如我们在Java笔记 (一)当中说过,可以通过继承体系来对程序进行解耦一样。针对于接口,很多时候能够做到更大程度上的解耦。
这是因为Java对于“继承”的使用思想以及“单继承”的限制造成的。想象一下:
假设我们在程序中的某个方法,使用针对于某一个继承体系的基类进行代码编写的方式。
这样做很好,因为我们很好的降低了,位于这个继承体系当中的所有类之间的耦合关系。
但是,也可能存在这样一种情况:有一个位于该继承体系之外的类,它的方法结构完全能够适用于我们定义的方法,但却不能够被该方法接受。
我们当然可以强行将该类也改用继承,唯一的缺点只是这样做不符合继承”is-a”的思想罢了。
但还有一种更更糟的情况,那就是:如果此类本身就已经继承了某个类了,那你就别无他法了。
这说明我们的解耦工作还不彻底,就如下面的例子演示的一样:
public class Test {
public static void main(String[] args) {
test(new Father());
test(new Son_1());
test(new Son_2());
test(new Son_3());
test(new A()); // 不允许这样做,因为A位于Father继承体系之外
}
static void test(Father f){
f.f1();
f.f2();
}
}
class Father {
void f1() {}
void f2() {}
}
class Son_1 extends Father {}
class Son_2 extends Father {}
class Son_3 extends Father {}
class A {
void f1() {}
void f2() {}
}
这个时候如果修改为针对于接口进行编程,就有了不同:
public class Test {
public static void main(String[] args) {
test(new Father());
test(new Son_1());
test(new Son_2());
test(new Son_3());
test(new A()); // 现在允许这样做了
}
static void test(I i) {
i.f1();
i.f2();
}
}
interface I {
void f1();
void f2();
}
class Father implements I {
public void f1() {}
public void f2() {}
}
class Son_1 extends Father {}
class Son_2 extends Father {}
class Son_3 extends Father {}
class A implements I {
public void f1() {}
public void f2() {}
}
这样,通过将方法test()改为针对于接口编程,我们又进一步的降低了程序的耦合性。
那么,再一进步思考这样一种情况:假设class A不是你能够修改的类,也就是说它可能来自于某个类库。
这个时候,我们应该怎么做才能实现上面的解耦效果呢?也没关系,可以使用适配器模式。
public class Test {
public static void main(String[] args) {
test(new Father());
test(new Son_1());
test(new Son_2());
test(new Son_3());
test(new ClassAdapter(new A())); // 现在允许这样做了
}
static void test(I i) {
i.f1();
i.f2();
}
}
interface I {
void f1();
void f2();
}
class Father implements I {
public void f1() {}
public void f2() {}
}
class Son_1 extends Father {}
class Son_2 extends Father {}
class Son_3 extends Father {}
class ClassAdapter implements I {
private A a;
ClassAdapter(A a){
this.a = a;
}
public void f1() {
a.f1();
}
public void f2() {
a.f2();
}
}
class A{
void f1(){};
void f2(){};
}
内部类的小知识
内部类是指那些嵌套在另一个类之中的类,它最大的长处就在于:
允许你把一系列逻辑相关的类组合在一起,而且可以根据需要控制它们的可见性。
一个常见的内部类的定义,简单得就像下面这样:
package com.tsr.demo;
public class TestClass {
class Inner{ //内部类
}
}
所以关于内部类的定义方式,确实没有什么值得好说的。我们来看一些有趣的东西。
关于内部类的对象创建
关于对于内部类的对象创建,有如下几种方式:
public class Outer {
class Inner { // 内部类
}
void f() {
Inner i = new Inner();
}
public static void main(String[] args) {
//Inner i = new Inner(); 不允许这样做
//第一种方式
Outer out = new Outer();
Outer.Inner i1 = out.new Inner();
// 第二种方式
Outer.Inner i2 = new Outer().new Inner();
}
}
从上面的代码中,我们不难发现,内部类对象的创建分为两种情况:
- 在外部类的实例方法中,进行对象的创建,这个时候可以和创建普通类的方式一模一样。
- 而在类的非静态方法之外的地方,想要创建内部类的对象,则必须保证先让其所属外部类的对象得到创建。
而造成这种现象的原因是因为,内部类的对象是依赖于其所属外部类对象而存在的。
当某个外部类对象创建了一个内部类的对象之后,该内部类的对象就会自动的捕获一个指向该外部类对象的引用。
内部类的妙用
访问外部类的私有成员
根据上面我们说到的,内部类对象持有一个创建它的外部类对象的引用。由此我们也就不难想到一个特点:
那就是内部类将能够访问外部类的所有成员,甚至包括私有的成员。这是不是很牛逼呢?
我们可以针对于内部类的这一特征,来针对某一些现实问题进行巧妙的处理呢?想象一下这样一种情况:
我们成立了一家公司,公司自然有财务部门来负责处理公司的一些财务状况。对应于面向对象的思想来说,转化为代码的表现形式可能类似于:
public class MyCompany {
private int money = 1000;
private FinanceDepartment fDepartment;
}
public class FinanceDepartment {
void doSomething() {}
}
问题在这个时候出现,财务部门的成员需要针对于“money”来进行工作。
简单的说就是:财务部门类必须能够拥有访问和修改这个变量的权利。
但是,公司的财务信息是很敏感和隐秘的,所以“money”字段出于隐私性的考虑,被申明为private。
于是财务部门无法访问到这个信息,也许你会说,我们可以通过提供公有的”get/set”方法来解决这个问题。
但这样做带来的隐患也显而易见,那就是任何其他类型都可以对该字段进行操作了。
这个时候,使用内部类就可以很好的解决这一问题:
public class MyCompany {
private int money = 1000;
class FinanceDepartment {
void doSomething() {
money = 2000;
}
}
}
这就像是公司头子对财务部门进行的公告:虽然“money”是隐秘的,但是没关系,咱们是内部同志的干活,我赋予你这个权利。
.this 的使用
回忆一下,当在继承中遇到了父类与子类中存在相同命名的域的时候,怎么来区分访问它们。是关键字”super”和”this”。
那么,想象一下如果在外部类和内部类存在相同命名的域的时候,又该怎么样区分呢?
没错,答案正是使用“.this”来进行访问,就像我们下面所做的这样:
public class Outer {
int x = 10;
class Inner {
int x = 20;
void f() {
System.out.println(Outer.this.x); // 10
System.out.println(x); // 20
System.out.println(this.x); // 20
}
}
}
其实,原理也很简单,我们说了内部类会自动的持有一个创建它的外部类对象的引用。
而事实上,该引用也正是通过以”外部类名.this”这样的方式而持有的。
而在内部类自身的引用仍然是通过“this”来持有的,所以说:
上面的代码中使用x与this.x都没有区别,只是一种隐式和显示的表现形式的区别而已。
通过内部类隐藏实现细节
现在我们来考虑这样一种情况,仍然以我们上面用到的公司与财务部门的例子来说:
对于内部类FinanceDepartment而言,其实它仍然可能暴露给其它很多成员。
因为它被定义为了包目录访问权限,所以这个时候,位于同一包内的其它任一类都是可以访问到它的。
但很多时候,我们希望类的实现细节是足够隐蔽的,通过内部类可以实现这一目的。
我们知道通常一个类只能被声明为两种访问权限:即公有的或者默认的包访问权限,
但内部类比较特别,Java的四种权限访问修饰符,它都能够使用。
结合于这一特质,再加上对于接口的使用,能够让我们最大限度的隐藏掉那些我们不希望暴露给别人的实现细节。
举例来说,我们有一个函数“doSomething”将要提供给消费者,同时但我们希望其实现细节能够最大程度的得以隐藏:
public class Outer {
private class Inner implements I {
@Override
public void doSomething() {
}
}
public I getInner() {
return new Inner();
}
}
interface I {
void doSomething();
}
在这里,我们将私有的内部类配合接口进行了搭配。它带来的好处显而易见:
- 消费者仍然可以访问到doSomething方法,只是变成了通过调用getInner().doSomething()的方式。
- 而好处在于,消费者此时却无法得知任何关于Inner类的实现细节,因为它甚至无法了解到有这样一个内部类的存在。
- 同时因为消费者得到的只是一个接口“I”类型的引用,由此也可以阻止其创建任何依赖于类型的编码。
(因为他们别无选择,只能针对于该接口进行编程,从而降低代码的耦合度。)
局部内部类的秘密
像我们之前说到的内部类,都是内部类最通常的表现形式。
而就像允许将类定义在类的内部一样,类也可以被定义在方法的作用域之中。通过这种方式定义的内部类,被称为局部内部类。
而被这样命名的原理,也同样不难理解。因为就和定义在方法中作用域内的变量被称为局部变量一样。
public class Outer {
int num = 10;
public int f() {
// 局部内部类
class Inner {
int x = 20;
int getSum() {
return num + x;
}
}
return new Inner().getSum();
}
}
上面的例子展示了一个局部内部类的基本表现形式,并且我们也可以从中观察到局部内部类值得被留意的一些特点:
- 与其它的局部成员一样,局部类的有效范围被限定在包含的代码块中,一旦超出该范围,该局部内部类就不能被访问了。
- 局部内部类不能被访问修饰符修饰,也就是说不能使用private、protected、public任一修饰符。
(因为作用域已经被限定在了当前所属的局部块中。)- 普通的内部类虽然能访问任何其所属外部类的成员,但访问不到其所属外部类所定义的方法当中的局部变量。
(这通常也是使用局部内部类的最常见原因)
而实际上,关于局部内部类我觉得其实有一个最值得弄清楚的问题,那就是:
为什么在局部内部类当中使用到的参数必须被声明为final
我们首先来看一段熟悉的代码:
public class Test {
void f(int x){
new Thread(new Runnable() {
@Override
public void run() {
int a = x +10;
}
}).start();
}
}
上面的代码,相信每一个使用java的人都熟悉不过,但初学时我们都可能会碰到这样的问题,就是上面的代码编译不过。
必须将方法f()的参数x改为final修饰后,才能够编译通过。在很长一段的时间内,我对这个问题都弄不清楚,只知道照着规则去做就是了。
今天,我们就来分析一下,Java为什么要求我们这样做,我们先不说原理,先看一段代码:
public class Outer{
public I f(){
final int x = 10;
class Inner implements I{
@Override
public void f1() {
int a = x + 10;
}
}
return new Inner();
}
interface I{
void f1();
}
}
相信看完上面的代码,机智的人已经能够猜到答案了。闲话少说,其实道理说透了也很简单,就像上例所演示的那样:
- 首先,我们在方法f()当中定义了一个变量x。
- 同时,我们在局部内部类当中使用到了这个变量。
- 最后,方法()返回了一个局部内部类Inner的对象。
到此,我们不难想到,如果变量x不为final。那么可能出现的情况就是:
- x在方法f()执行完毕后,随着栈帧出栈,也就是说从内存中清除了。
- 但是方法f()却返回了一个局部内部类Inner对象,也就是说,在程序的其它地方,该局部内部类对象仍然可能会被使用。
- 现在造成的尴尬局面就是:Inner对象仍然存在,仍然可能使用到变量x,但x却已经从内存中清除了。
而将x用final修饰为什么能够解决这一问题?很简单,程序中的final域将并不存放在虚拟机栈中了。
(final域将随着类被加载,被存放进方法区里的所属区域,自然也就避免了“出栈”而造成的内存丢失)。
匿名内部类
其实,这个玩意个人看来并没什么太值得说的。但既然是在总结内部类。那就还是来看一看。
这东西相信不管了不了解匿名内部类这个名词,但实际上编写代码的过程中都写过N多次了。
因为Java里的多线程,经常就通过在这样的方式来创建:
public class Test{
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
}
});
t.start();
}
}
这里,对Tread类的构造器传入的参数new Runnable。。。。就是一个匿名内部类。
到此,其实也就不难想象,所谓的匿名内部类就和其名字一样,就是指一个没有“名字”的类。
因为我们也可以把上面的匿名内部类给他一个名字,就像下面这样:
public class Test {
public static void main(String[] args) {
class MyRunnable implements Runnable {
@Override
public void run() {
// TODO Auto-generated method stub
}
}
Thread t = new Thread(new MyRunnable());
t.start();
}
}
如你所见,现在当初的匿名内部类不再是“黑户”了,他有了自己的身份“MyRunnable”。
所以,对于匿名内部类,我们值得记住的可能主要就是以下两点:
- 匿名内部类是对于内部类的一种简写形式。
- 匿名内部类必须是基于继承自某个外部抽象类或者实现一个接口的基础上,才能实现。
静态内部类
顾名思义,静态内部类就是指被static所修饰的内部类。就像下面这样:
public class Outer {
static class Inner{
}
}
与普通的内部类最大的不同就在于,静态内部类不会保存有一个指向其外部内对象的引用。
也就是说,与普通的内部类相比,它与外部类并没有那么密切的联系。
与其说是内部类,不如说它只是借助外部类寻找一个自己存在的区域。
所以它更像是嵌套在另一个类的当中的一个独立的类(唯一的不同可能就在于它可以访问外部类当中的私有的静态域)。
这也就是为什么,静态内部类又被称为“嵌套类”。
于是,关于静态内部类,我们可以归纳出以下特点:
- 要创建静态内部类的对象,并不需要依赖其外部类对象。
- 无法在静态内部类中,访问其外部类的非静态部分。
- 普通的内部类无法包含static字段或方法,但静态内部类则可以。
正对应于第三条,普通的内部类无法嵌套静态内部类;但静态内部类则可以包含普通的内部类。
接口也可以嵌套静态内部类。
我们说过接口中的域默认都是static的,所以在接口内创建的内部类都将成为嵌套类。
而这样做的意义在于:假设你提供了一个接口,同时又希望为这个接口各个不同的实现者提供一些共有的方法。
那么,在接口中定义嵌套类来提供这些方法,就将是很好的选择。
内部类的继承
虽然这种情况在实际开发中比较少见,但确实存在着内部类的继承。就像下面这样:
public class Outer {
class Inner{
}
}
class Another extends Outer.Inner{
public Another(Outer o) {
o.super();
}
}
可以看到,就像在上面的例子里演示的一样。在继承一个内部类的时候,需要注意的是:
- 声明继承的时候,要写出该内部类的全限定名(也就是OuterClassName.InnerClassName这样的形式)。
- 对于实现继承的类的构造器,必须传入一个继承的内部类其所属外部类的对象引用。
- 并且,在构造器中,还需要通过外部类引用.super()这样的方式先构造出继承的内部类所属外部类的对象。
(这样做是因为:当使用继承的时候,构造子类对象之前,将先完成其父类对象的构造。同时,
内部类又将隐式的持有其所属外部类的对象引用,所以还需要先秘密完成其外部类引用的初始化。)
为什么使用内部类
我们前面已经总结了如此之多关于内部类的知识以及使用方式。其实,在实际使用中,我们最常在什么时候才会使用到内部类呢?
当遇到“当前类当中需要的某个操作,应当放在另一个单独的特殊类中更为合适。因为要保持类足够的专用性;
但不巧的是,这些操作又与当前的类有着密切联系(例如会使用到当前类当中的成员(包括私有成员)等等)”这种情况。
(出自《Java2 学习指南》)借助内部类实现“多重继承”。也就是假如现在你想要继承多个类,而复用它们的一些功能。
但由于Java中只允许单继承,就会造成一定麻烦。这个时候,就可以在类中定义多个内部类分别进行继承,从而实现目的。
(出自《Thinking in java》)当然就像《Thinking in java》里还谈及有其它更多的使用情况一样,但个人觉得前两种就是内部类被使用的最常见的原因。
以上是关于JAVA 笔记的主要内容,如果未能解决你的问题,请参考以下文章