JavaSE-Adventure(VII) Java & OOP 面向对象程序设计
Object-oriented programming (OOP)
概述
OOP 是围绕数据, 对象, 而非过程和逻辑的一种软件设计模型的计算机编程范式。它以类或对象作为组织代码的基本单元,并将封装、抽象、继承和多态四大特性,作为代码设计和实现的基石。
面向对象是一种对现实世界理解和抽象的方法,是计算机编程技术发展到一定阶段后的产物。
面向对象是相对于面向过程来讲的,面向对象方法,把相关的数据和方法组织为一个整体来看待,从更高的层次来进行系统建模,更贴近事物的自然运行模式。
面向对象是面向对象的程序设计的核心,它由描述状态的属性(变量)和用来实现对象行为的方法(函数)组成,完成了从数据模型到处理模型的结合与统一。
面向对象方法论的出发点和基本原则是尽可能模拟人类习惯的思维方式,使开发软件的方法与过程尽可能接近人类认识世界解决问题的方法与过程。
也就是使描述问题的问题空间(也称为问题域)与实现解法的解空间(也称为求解域)在结构上尽可能一致。这样就解决了系统在分析过程中获得的分析模型与设计过程中所获得的设计模型进行转换时,由于理解上的差异而造成的系统不稳定性。
面向对象方法论中产生的设计模型是分析模型的进一步完善和细化,使得模型之间的转换成为一种平滑的过渡。
背景
了解OOP的诞生过程, 演进, 同样帮助我们更加了解这种编程思想。
面临的问题
面向对象是在
结构化设计方法
出现很多问题的情况下应运而生的。
结构化设计方法求解问题的基本策略是从功能的角度审视问题域。它将应用程序看成实现某些特定任务的功能模块,其中子过程是实现某项具体操作的底层功能模块。在每个功能模块中,用数据结构描述待处理数据的组织形式,用算法描述具体的操作过程。面对日趋复杂的应用系统,这种开发思路在下面几个方面逐渐暴露了一些弱点。
1. 审视问题域的视角
在现实世界中存在的客体是问题域中的主角,所谓客体是指客观存在的对象实体和主观抽象的概念。
例如,对于一个学校学生管理系统来说,始终是围绕学生和老师这两个客体实施。在自然界,每个客体都具有一些属性和行为,例如学生有学号、姓名、性别等属性,以及上课、考试、做实验等行为。因此,每个个体都可以用属性和行为来描述。
- 人类观察问题的视角: 客体
- 客体的属性: 反应客体在某一时刻的状态
- 客体的行为: 反映客体能从事的操作, 这些操作附在客体之上并能用来设置、改变和获取客体的状态。
任何问题域都有一系列的客体,因此解决问题的基本方式是让这些客体之间相互驱动、相互作用,最终使每个客体按照设计者的意愿改变其属性状态。
- 结构化设计方法
结构化设计方法所采用的设计思路不是将客体作为一个整体,而是将依附于客体之上的行为抽取出来(属性与操作分离, 相互独立),以功能为目标来设计构造应用系统。
这种做法导致在进行程序设计的时候,不得不将客体所构成的现实世界映射到由功能模块组成的解空间中,这种变换过程,不仅增加了程序设计的复杂程度,而且背离了人们观察问题和解决问题的基本思路。
另外,再仔细思考会发现,在任何一个问题域中,客体是稳定的,而行为是不稳定的。
例如,不管是国家图书馆,还是学校图书馆,还是国际图书馆,都会含有图书这个客体,但管理图书的方法可能是截然不同的。
结构化设计方法将审视问题的视角定位于不稳定的操作之上,并将描述客体的属性和行为分开,使得应用程序的日后维护和扩展相当困难,甚至一个微小的变动,都会波及到整个系统。
2. 抽象级别
抽象是人类解决问题的基本法宝。良好的抽象策略可以控制问题的复杂程度, 增强系统的通用性和可扩展性。
抽象主要包括: 过程抽象和数据抽象。
结构化设计方法应用的是过程抽象。所谓过程抽象是将问题域中具有明确功能定义的操作抽取出来,并将其作为一个实体看待。这种抽象级别对于软件系统结构的设计显得有些武断,并且稳定性差,导致很难准确无误地设计出系统的每一个操作环节。一旦某个客体属性的表示方式发生了变化,就有可能牵扯到已有系统的很多部分。
而数据抽象是较过程抽象更高级别的抽象方式,将描述客体的属性和行为绑定在一起,实现统一的抽象,从而达到对现实世界客体的真正模拟。
3. 封装体
封装是指将现实世界中存在的某个客体的属性与行为绑定在一起,并放置在一个逻辑单元内。该逻辑单元负责将所描述的属性隐藏起来,外界对客体内部属性的所有访问只能通过提供的用户接口实现。
这样做既可以实现对客体属性的保护作用,又可以提高软件系统的可维护性。只要用户接口不改变,任何封装体内部的改变都不会对软件系统的其他部分造成影响。
结构化设计方法没有做到客体的整体封装,只是封装了各个功能模块,而每个功能模块可以随意地对没有保护能力客体属性实施操作,并且由于描述属性的数据与行为被分割开来,所以一旦某个客体属性的表达方式发生了变化,或某个行为效果发生了改变,就有可能对整个系统产生影响。
4. 可重用性
可重用性标识着软件产品的可复用能力,是衡量一个软件产品成功与否的重要标志。
当今的软件开发行业,人们越来越追求开发更多的、更有通用性的可重用构件,从而使软件开发过程彻底改善,即从过去的语句级编写发展到构件组装,从而提高软件开发效率,推动应用领域迅速扩展。
然而,结构化程序设计方法的基本单位是模块,每个模块只是实现特定功能的过程描述,因此,它的可重用单位只能是模块。例如,在C语言编写程序时使用大量的标准函数。但对于现代的软件开发来说,这样的重用力度显得微不足道,而且当参与操作的某些数据类型发生变化时,就不能够再使用那些函数了。因此,渴望更大力度的可重用构件是如今应用领域对软件开发提出的新需求。
类与对象
类是现实世界或思维世界中的实体在计算机中的反映,它将数据以及这些数据上的操作封装在一起。
对象是具有类类型的变量。类和对象是面向对象编程技术中的最基本的概念。
类与对象的关系:
类是对象的抽象,而对象是类的具体实例。类是抽象的,不占用内存,而对象是具体的,占用存储空间。类是用于创建对象的蓝图,它是一个定义包括在特定类型的对象中的方法和变量的软件模板。
程序执行期间具有不同的状态(成员变量值不同),而其他方面都相似的对象会被分组到对象的类中。
类描述了具有相同特性(数据元素)和行为(功能)的对象集合。
由类构造 (construct) 对象的过程称为创建类的实例 (instance)。
对象的含义
对象的含义是指具体的某一个事物,即在现实生活中能够看得见摸得着的事物。
在面向对象程序设计中,对象包含两个含义,其中一个是数据,另外一个是动作。
- 万物皆对象
把对象看作是特殊的变量。- 对象的状态:这个变量可以存储数据(数据)
- 对象的行为:可以在自身上执行操作 (方法)
OOP 的思想就是将所有待求解的问题的任何概念化构件 都抽取成对象,由对象去解决问题。
-
程序是对象的集合
程序是对象的集合,对象之间通过发送消息告知彼此的目的(消息传递: 对某个对象的方法的调用请求) -
可以有由其他对象构成的存储
可以创建包含现有对象的方式来创建新类型的对象 -
每个对象都有其类型
每个对象都是某个类(class) 的一个实例(instance)。 -
类(类型)
类区别于其他类的特性就是可以发什么消息给它。 -
某一特定类型的对象可以接收同样的消息
圆形对象一定能接收发送给几何形对象的消息。这意味着可以编写与几何形交互并处理所有与几何形性质相关的事务的代码。(可替代性 substitutability)
static 关键字
主要修饰目标:成员变量、成员方法、代码块
成员变量、方法被static 修饰后表示隶属该类,可通过类名直接调用。
一个既是final 又是 static 的域只占据一段不能修改的存储空间(编译期常量)。
深拷贝与浅拷贝
- 浅拷贝
浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。 - 深拷贝
深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
面向对象特征
对象唯一性
每个对象都有自身唯一的标识,通过这种标识,可找到相应的对象。在对象的整个生命期中,它的标识都不改变,不同的对象不能有相同的标识。
抽象性
抽象性是指将具有一致的数据结构(属性)和行为(操作)的对象抽象成类。一个类就是这样一种抽象,它反映了与应用有关的重要性质,而忽略其他一些无关内容。任何类的划分都是主观的,但必须与具体的应用有关。
在Java 世界,使用 interface 或 abstract 来实现抽象。
抽象的意义:良好的抽象策略可以控制问题的复杂程度, 增强系统的通用性和可扩展性。
封装性
基本概念
封装是类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(或者叫函数)来访问内部信息或者数据。
封装需要对类的访问进行控制的前提是编程语言提供了访问权限控制的语法机制
封装的意义:
-
提高代码的可读性,可维护性。如果没有封装,对类中属性的修改逻辑会散落在代码中的各个角落
-
提高类的易用性。每个类只提供有限的方式暴露其必要的操作,如果把类中所有的属性都暴露给了调用者,想要正确的使用属性,就必须对业务有较深的认识,对调用者来说也是一种负担
访问控制权限
-
public
它具有最大的访问权限,可以访问任何一个在CLASSPATH下的类、接口、异常等。它往往用于对外的情况,也就是对象或类对外的一种接口的形式。 -
protected
它主要的作用就是用来保护子类的。它的含义在于子类可以用它修饰的成员,其他的不可以,它相当于传递给子类的一种继承的东西。(或同一包内可以访问被protected 修饰的域或方法) -
default
有的时候也称为friendly,它是针对本包访问而设计的,任何处于本包下的类、接口、异常等,都可以相互访问,即使是父类没有用protected修饰的成员也可以。 -
private
它的访问权限仅限于类的内部,是一种封装的体现,例如,大多数的成员变量都是修饰符为private的,它们不希望被其他任何外部的类访问。
注意:Java的访问控制是停留在编译层的,也就是它不会在.class文件中留下任何的痕迹,只在编译的时候进行访问控制的检查。其实,通过反射的手段,是可以访问任何包下任何类中的成员,例如,访问类的私有成员也是可能的。
继承性
基本概念
继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
继承性是子类自动共享父类数据结构和方法的机制,这是类之间的一种关系。在定义和实现一个类的时候,可以在一个已经存在的类的基础之上来进行,把这个已经存在的类所定义的内容作为自己的内容,并加入若干新的内容。
采用继承性,提供了类的规范的等级结构。通过类的继承关系,使公共的特性能够共享,提高了软件的重用性。
继承的意义:代码复用
继承带来的问题:
- 继承层次过深,影响代码的可读性和可维护性
- 继承使得父类和子类耦合,针对父类的修改,也会影响到子类
继承的替代方案:
一般认为,组合优于继承,使用松耦合的组合来替换强耦合的继承是一种主流的实现方式。
继承的特性
- 子类拥有父类非 private 的属性、方法。
- 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。(Override 重写)
- Java 的继承是单继承,但是可以多重继承,单继承就是一个子类只能继承一个父类,多重继承就是,例如 B 类继承 A 类,C 类继承 B 类,所以按照关系就是 B 类是 C 类的父类,A 类是 B 类的父类,这是 Java 继承区别于 C++ 继承的一个特性。
- 提高了类之间的耦合性(继承的缺点,耦合度高就会造成代码之间的联系越紧密,代码独立性越差)。
super / this 关键字
我们可以通过super
关键字来实现对父类成员的访问,用来引用当前对象的父类。
super.父类成员变量
super.父类实例方法
super注意点:
- super调用父类的构造方法,必须在构造方法的第一个
- super必须只能出现在子类的方法或者构造方法中!
- super和this 不能同时调用构造方法!
this
关键字指向的是当前对象的引用。
this.当前类成员变量
this.当前类实例方法
super 与 this 在构造器中的使用
类的构建过程是从基类向外扩散的,直到Object类的构造。也就是通过new 构建一个对象时,其实在构造器的第一行会隐式的加上super() 去调用父类的构造方法,对父类进行初始化(如果父类没有无参构造时会编译报错)。
如果使用super显示的调用父类构造,就表示根据super() 参数列表查找父类构造函数,并调用。
但该类构造中出现了this()时,会在该构造中调用本类的其他构造,但最终还是会在调用链的底端调用到父类的构造。所以如果super()和this()同时存在,那么就会出现两次初始化父类。第一次是super()调用父类构造,第二次是this()调用链底端的子类构造里调用父类构造,这样就造成两次调用super。
因此super和this 不能同时调用构造方法!
方法重写(Override)
方法重写的要求:
- 子类重写方法的名称和形参列表必须与父类重写方法一致
- 子类重写方法的返回值类型范围小于等于父类
- 子类重写方法的修饰符权限应该与父类的修饰符权限相同或者更大
- 子类重写方法声明抛出的异常应该与父类被重写的方法申明抛出的异常一样或者范围更小
final 关键字
final 可以用来修饰(包括类属性、对象属性、局部变量和形参)、方法(包括类方法和对象方法)和类。
- 修饰类:不能被继承的类(final修饰的类:String、8个基本数据的包装类)
- 修饰方法:不能被子类重写的方法;
- 修饰变量:不可改变的变量,即变量第一次被赋值后不可再被赋值
- 修饰成员变量:定义时必须赋值;
- 修饰局部变量:定义时可以不赋值,但是在使用前必须赋值;
- 修饰参数变量:方法调用传递参数时,形参被第一次赋值;
- 修饰引用类型变量:不可被再次赋值,但该引用变量可以改变变量内的成员。
多态性
多态,顾名思义,表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。
- 多态性是指相同的操作可作用于多种类型的对象上并获得不同的结果。不同的对象,收到同一消息可以产生不同的结果,这种现象称为多态性。
- 多态性允许每个对象以适合自身的方式去响应共同的消息。
多态存在的意义:
- 多态提高的代码的可扩展性。多态可以灵活地让新的逻辑可以通过继承父类或实现接口的方法加入到代码逻辑中来
- 多态提高的代码的复用性。同一段代码逻辑可以被多种具体的实现来调用
- 多态是很多设计原则、设计模式和编程技巧的代码实现基础
多态的特点
- 对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
- 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
- 多态不能调用“只在子类存在但在父类不存在”的方法;
- 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。
Java实现多态的必要条件
继承,方法重写,向上转型。
-
继承
在多态中必须存在有继承关系的子类和父类。 -
方法重写
子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。 -
向上转型
在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。
方法调用绑定
静态绑定
在程序执行前方法已经被绑定(在编译过程中就已经明确这个方法是哪个类的方法),此时,由编译器或其它连接程序实现。
static & final & private
-
private 方法
首先它不能被继承,既然不能被继承那么就没办法通过它子类的对象来调用,而只能通过这个类自身的对象来调用。因此就可以说private方法和定义这个方法的类绑定在了一起。 -
final 方法
虽然可以被继承,但不能被重写(覆盖),虽然子类对象可以调用,但是调用的都是父类中所定义的那个final方法,(由此我们可以知道将方法声明为final类型,一是为了防止方法被覆盖,二是为了有效地关闭Java中的动态绑定)。 -
static 方法
static方法可以被子类继承,但是不能被子类重写(覆盖),但是可以被子类隐藏。(这里意思是说如果父类里有一个static方法,它的子类里如果没有对应的方法,那么当子类对象调用这个方法时就会使用父类中的方法。而如果子类中定义了相同的方法,则会调用子类的中定义的方法。唯一的不同就是,当子类对象上转型为父类对象时,不论子类中有没有定义这个静态方法,该对象都会使用父类中的静态方法。因此这里说静态方法可以被隐藏而不能被覆盖。这与子类隐藏父类中的成员变量是一样的。隐藏和覆盖的区别在于,子类对象转换成父类对象后,能够访问父类被隐藏的变量和方法,而不能访问父类被覆盖的方法)。 -
构造方法
构造方法也是不能被继承的(子类是通过隐藏的super()来调用父类的无参构造方法,来完成对父类的初始化),因此编译时也可以知道这个构造方法到底是属于哪个类。
动态绑定 (后期绑定)
动态绑定:在运行时根据具体对象的类型进行绑定
在运行时根据具体对象的类型进行绑定。若一种语言实现了后期绑定,同时必须提供一些机制,可在运行期间判断对象的类型,并分别调用适当的方法。也就是说,编译器此时依然不知道对象的类型,但方法调用机制能自己去调查,找到正确的方法主体。
-
编译器查看对象的声明类型和方法名;
-
编译器查看调用方法时提供的参数类型。例如x.f(“hello”)(x:类名;f:方法名,下同),编译器将会挑选f(String),而不是f(int),不过存在类型转换(int可以转换成double,Manager可以转换成Employee,等等)。如果编译器没找到参数类型匹配的方法,或者发现有多个方法与之匹配,就会报告一个错误。至此,编译器获得了需要调用的方法名字和参数类型。
-
采用动态绑定调用方法的时候,一定调用与所引用对象的实际类型最合适的类的方法。如果x的实际类型是D,它是C类的子类,如果D定义了一个方法f(String),就直接调用它,否则将在D类的超类(父类)即C类中寻找f(String),以此类推。
每次调用方法都要进行搜索,时间开销太大,所以虚拟机预先为每个类创建一个方法表(method table),其中列出了所有方法的签名和实际调用的方法。这样在调用方法的时候,只需要查找这个表即可。
方法的重载是多态性的一种体现?
对于多态,只有等到方法调用的那一刻, 解释运行器才会确定所要调用的具体方法,这称为“晚绑定”或“动态绑定” 。
重载
-
发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载。
-
与返回值类型无关,只看参数列表,且参数列表必须不同。 (参数个数或参数类型)。调用时,根据方法参数列表的不同来区别。
-
编译器根据方法不同的参数表,对同名方法的名称做修饰。对于编译器而言,这些同名方法就成了不同的方法。它们的调用地址在编译期就绑定了。
-
因此对于重载而言,在方法调用之前,编译器就已经确定了所要调用的方法,这称为“早绑定”或“静态绑定”。
重写
- 发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。重载对返回类型没有特殊的要求。
所以说,方法的重载并不是多态性的一种体现
如果脱离Java语言范畴,在广义的计算机语言范畴里来说,方法重载毫无疑问是一种多态,同一个方法名,根据不同的传参执行不同的行为。
抽象类与接口
接口和抽象类有什么共同点和区别?
共同点:
- 都不能被实例化。
- 都可以包含抽象方法。
- 都可以有默认实现的方法(Java 8 可以用 default 关键在接口中定义默认方法)。
区别:
- 接口主要用于对类的行为进行约束,你实现了某个接口就具有了对应的行为。抽象类主要用于代码复用,强调的是所属关系(比如说我们抽象了一个发送短信的抽象类,)。
- 一个类只能继承一个类,但是可以实现多个接口。
- 接口中的成员变量只能是 public static final 类型的,不能被修改且必须有初始值,而抽象类的成员变量默认 default,可在子类中被重新定义,也可被重新赋值。
嵌套类 (内部类)
Java允许将一个类定义在另一个类内部。这样的类称为嵌套类(Nested Class)。
嵌套类可分为两部分:
- 非静态嵌套类(Non-static nested classes, 也叫内部类(inner classes)
- 成员内部类(Member inner class)
- 匿名内部类(Anonymous inner class)
- 本地内部类(Local inner class)
- 静态嵌套类(Static nested classes)
An inner class is a part of a nested class.
class OuterClass {
...
class InnerClass {
...
}
static class StaticNestedClass {
...
}
}
嵌套类作为外部类的一个成员存在。作为外部类的一个成员,嵌套类可以被private, public, protected, or package private修饰。外部类只可以被 public or package private修饰。
区别:
内部类的优势
-
对仅用在一处地方的类进行逻辑分组
如果一个类只对另外一个类有用,逻辑上他被嵌入这个类。内嵌“帮助类”可以使包更合理化。 -
提升封装性
考虑两个顶级类,A和B,B需要访问A的可能声明为私有的成员。通过把类B隐藏在类A中,B可以访问A的私有成员。此外B也可以从外界隐藏。 -
提升可读性和可维护性
嵌套小类在顶级类中,可以使代码更接近使用的地方。
类型 | 描述 |
---|---|
成员内部类 | 定义在一个类的内部, 方法的外部 |
匿名内部类 | 为了实现接口或是继承类而创建的类, 编译器会生产其类名 |
本地内部类 | 方法内部定义的类 |
静态嵌套类 | 定义在一个类内部的静态类 |
嵌套接口 | 定义在类或接口内部的接口 |
成员内部类 (Member Inner Class)
定义在类内部, 方法外部的一个非静态类, 成为成员内部类(普通内部类)。
成员内部类可以被访问控制符修饰(public, default, private, protected)。
语法
class Outer{
//code
class Inner{
//code
}
}
示例:
class TestMemberOuter1{
private int data=30;
class Inner{
void msg(){System.out.println("data is "+data);}
}
public static void main(String args[]){
TestMemberOuter1 obj=new TestMemberOuter1();
TestMemberOuter1.Inner in=obj.new Inner();
in.msg();
}
}
// output: data is 30
实例化成员内部类
内部类的对象或实例的创建总是在外部类的实例内部。
语法:
外部类引用.new 成员内部类构造器();
obj.new Inner();
底层实现
TestMemberOuter1.java
经过编译后生成两个.class 文件
TestMemberOuter1$Inner.class TestMemberOuter1.class
如果想要实例化内部类,必须先创建外部类的实例。在这个例子中,内部类的实例是在外部类实例的内部创建的。
通过外部类实例创建的成员内部类,在成员内部类内部拥有外部类的实例,这也是为什么它能够访问外部类的成员,即使是被private 修饰的成员。
内部类经过编译后的样子:
class TestMemberOuter1$Inner
{
final TestMemberOuter1 this$0;
TestMemberOuter1$Inner()
{ super();
this$0 = TestMemberOuter1.this;
}
void msg()
{
System.out.println((new StringBuilder()).append("data is ")
.append(TestMemberOuter1.access$000(TestMemberOuter1.this)).toString());
}
}
匿名内部类 (Anonymous Inner Class)
Java 匿名内部类是一个没有名字并且仅有一个实例被创建的内部类。
匿名内部类通常用于重写一个类的方法(抽象方法或子类方法),或是实现接口方法,但是又并不创建(继承/实现)一个新的类。
匿名内部类实现方式:
- 类 (抽象或具体)
- 接口
语法 和底层实现
类
语法:
abstract class Person{
abstract void eat();
}
class TestAnonymousInner{
public static void main(String args[]){
Person p=new Person(){
void eat(){System.out.println("nice fruits");}
};
p.eat();
}
}
//output : nice fruits
底层实现:
Person p=new Person(){
void eat(){System.out.println("nice fruits");}
};
这段代码的含义是:
- 一个类被创建,但是他的名字由编译期决定,并且该类继承自
Person
,并提供了抽象方法eat()
的实现 - 一个匿名类的对象被创建,并将引用指向
p
,该变量的类型是Person
匿名内部类经过编译后生成的.class的样子:
static class TestAnonymousInner$1 extends Person
{
TestAnonymousInner$1(){}
void eat()
{
System.out.println("nice fruits");
}
}
接口
语法:
interface Eatable{
void eat();
}
class TestAnnonymousInner1{
public static void main(String args[]){
Eatable e=new Eatable(){
public void eat(){System.out.println("nice fruits");}
};
e.eat();
}
}
//output : nice fruits
底层实现:
Eatable p=new Eatable(){
void eat(){System.out.println("nice fruits");}
};
这段代码的含义是:
- 一个类被创建,但是他的名字由编译期决定,并且该类实现了
Eatable
接口,并实现了接口方法eat()
- 一个匿名类的对象被创建,并将引用指向
p
,该变量的类型是Eatable
匿名内部类经过编译后生成的.class的样子:
static class TestAnonymousInner1$1 implements Eatable
{
TestAnonymousInner1$1(){}
void eat(){System.out.println("nice fruits");}
}
本地内部类 (Local Inner Class)
Java 中,一个类在方法中被定义,成为本地内部类(Local Inner Class)。严谨来说本地内部类是被定义在代码块(block)之中,通常来说是方法体中,但也有可能是 for 循环,if 子句。
本地内部类不属于外部类的成员,它的生命周期属于定义它时所处的代码块,因此本地内部类不能被访问控制符修饰。但是可以被final 或是abstract 修饰。
本地内部类拥有外部类成员的访问权限。
如果想要调用本地内部类的方法,需要在方法内实例化该类。
语法
public class localInner1{
private int data=30;//instance variable
void display(){
class Local{
void msg(){System.out.println(data);}
}
Local l=new Local();
l.msg();
}
public static void main(String args[]){
localInner1 obj=new localInner1();
obj.display();
}
}
// output:30
本地内部类经过编译后生成的.class的样子:
class localInner1$Local
{
final localInner1 this$0;
localInner1$Local()
{
super();
this$0 = Simple.this;
}
void msg()
{
System.out.println(localInner1.access$000(localInner1.this));
}
}
底层实现
本地内部类经过编译后生成的.class的样子:
class localInner1$Local
{
final localInner1 this$0;
localInner1$Local()
{
super();
this$0 = Simple.this;
}
void msg()
{
System.out.println(localInner1.access$000(localInner1.this));
}
}
特性
- 本地内部类不能被访问控制符修饰
- 如果想要调用本地内部类的方法,必须在方法内实例化该类并调用
- 直到JDK 1.8之前,本地内部类无法访问非final的局部变量(但仍无法修改, Java 8新特性:effectively final,隐式添加final)
public class TestMemberOuter2 {
private int data=30;//instance variable
void display(){
int value=50;//local variable must be final till jdk 1.7 only
class Local{
void msg(){
// value = 100; effectively final, can't modify
System.out.println(value);
}
}
Local l=new Local();
l.msg();
}
public static void main(String args[]){
TestMemberOuter2 obj=new TestMemberOuter2();
obj.display();
}
}
// output: 50
静态嵌套类 (Static Nested Class)
在一个类内部定义的静态类成为静态嵌套类(静态内部类)。
特性
-
静态嵌套类无法访问非静态的外部类成员变量及方法,同样可以访问外部类的静态成员,即使是被private 修饰的。
-
静态内部类可以直接通过外部类类名访问。
语法
class TestOuter1{
static int data=30;
static class Inner{
void msg(){System.out.println("data is "+data);}
}
public static void main(String args[]){
TestOuter1.Inner obj=new TestOuter1.Inner();
obj.msg();
}
}
// output: data is 30
在这个例子中,我们需要调用静态内部类的实例方法,我们需要先实例化静态内部类。这里我们直接通过外部类的类名去调用静态内部类的构造器。
静态成员变量,方法,或是类,都无需经过实例进行访问,可直接通过类访问。
实现原理
静态内部类经过编译后生成的.class的样子:
static class TestOuter1$Inner
{
TestOuter1$Inner(){}
void msg(){
System.out.println((new StringBuilder()).append("data is ")
.append(TestOuter1.data).toString());
}
}
因为是静态内部类,是外部类的静态成员,所以该内部类没有外部类的实例引用,因此无法访问外部类的实例成员
当需要调用静态内部类的静态方法是,同样无需实例化外部类,也无需实例化静态内部类:
public class TestOuter2{
static int data=30;
static class Inner{
static void msg(){System.out.println("data is "+data);}
}
public static void main(String args[]){
TestOuter2.Inner.msg();//no need to create the instance of static nested class
}
}
嵌套接口 (Nested Interface)
定义在一个类或是接口内部的接口,被称为嵌套接口(Nested Interface)。
嵌套接口通常用于组织有关联关系的接口,以便于维护。
嵌套接口必须通过外部接口或类进行访问。
注意点:
- 嵌套接口如果定义在接口内部,则必须声明为
public
。若是在定义在类内部,则可以是任意访问控制符。 - 嵌套类被定义成static
语法
interface interface_name{
...
interface nested_interface_name{
...
}
}
class class_name{
...
interface nested_interface_name{
...
}
}
示例:
interface Showable{
void show();
interface Message{
void msg();
}
}
class TestNestedInterface1 implements Showable.Message{
public void msg(){System.out.println("Hello nested interface");}
public static void main(String args[]){
Showable.Message message=new TestNestedInterface1();//upcasting here
message.msg();
}
}
// output: hello nested interface
通过上面例子看出,我们通过访问外部接口Showable
来直接访问Message
接口。
- 嵌套接口必须通过外部接口或类进行访问
- 嵌套类被定义成static
嵌套接口最经典的应用是:集合框架中的Map.Entry。
Entry
是 Map
的嵌套接口。通过Map.Entry
进行访问。
public interface Map<K,V> {
//...
interface Entry<K,V> {
K getKey();
V getValue();
//...
}
}
底层实现
public static interface Showable$Message
{
public abstract void msg();
}
同样我们可以将接口嵌套在类中:
class A{
interface Message{
void msg();
}
}
class TestNestedInterface2 implements A.Message{
public void msg(){System.out.println("Hello nested interface");}
public static void main(String args[]){
A.Message message=new TestNestedInterface2();//upcasting here
message.msg();
}
}
// output: hello nested interface
也能嵌套类在接口中:
interface M{
class A{}
}
参考:
百度百科:面向对象:https://baike.baidu.com/item/%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1/2262089
Javatpoint: Inner Class:https://www.javatpoint.com/java-inner-class