Java 的基础知识和细节纪录。如有谬误,请不吝赐教。
第一个 Java 程序
编写/编译/执行
|
|
将上面的文本保存为 HelloWorld.java
,接下来编译该 Java 源文件来生成字节码。
|
|
-d destdir
是javac
命令的选项,用以指定编译生成的字节码文件的存放路径。如果省略,则目标文件存放在当前路径下。srcFile
是 Java 源文件所在的位置。
这样就会生成字节码文件 HelloWorld.class
运行
|
|
注意是类名,没有 .java 后缀,也没有 .class 后缀。
程序设计思想
面向过程
|
|
面向对象
|
|
面向对象的语句更接近自然语言的语法:主语、谓语、宾语一目了然,十分直观。
基于对象
也使用了对象的概念,但和面向对象不同的是,无法利用现有的对象模版产生新的对象类型,继而产生新的对象。
判断一门语言是否是面向对象的,通常使用“继承”和“多态”加以判断。“面向对象”和“基于对象”都实现了“封装”的概念,但是“基于对象“没有实现这些。
数据类型和运算符
分隔符
|
|
标识符规则
标识符可以由字母、数字、_
和美元符 $
组成,其中数字不能打头。
Java 关键字
1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|
abstract | continue | for | new | switch |
assert | default | if | package | synchronized |
boolean | do | goto | private | this |
break | double | implements | protected | throw |
byte | else | import | public | throws |
case | enum | instanceof | return | transient |
catch | extends | int | short | try |
char | final | interface | static | void |
class | finally | long | strictfp | volatile |
const | float | native | super | while |
另外,Java 还提供了三个特殊的直接量 (literal): true, false, null,Java 语言的标识符也不能使用这三个特殊的直接量。
数据类型
Java 是强类型语言:
- 所有的变量必须先声明,后使用。
- 指定类型的变量只能接受类型与之匹配的值。这意味着每个变量和每个表达式都有一个在编译时就确定的类型。
强类型语言可以在编译时进行更加严格的语法检查,从而减少错误。
Java 语言支持两种类型:
- 基本类型 (Primitive Type):
- boolean 类型:值只能是
true
和false
不能用 0 或者非 0 代表,其他基本数据类型的值也不能转换成boolean
类型。 - 数值类型:
- 整数类型:
byte
1 个字节、short
2 个字节、int
4 个字节、long
8 个字节。 - 字符类型:
char
2 个字节,支持 16 位的 Unicode 字符 - 浮点类型:
float
4 个字节、double
8 个字节
- 整数类型:
- boolean 类型:值只能是
- 引用类型 (Reference Type):类、接口和数组类型,还有一种特殊的
null
类型。对象包括实例和数组两种,实际上,引用类型变量就是一个指针。null
只能转换成引用类型,不能转换成基本类型,因此不要把一个null
值赋给基本数据类型的变量。
|
|
基本类型的类型转换
自动类型转换
表数范围小的可以像表数范围大的进行自动类型转换。
byte
< short
< int
< long
< float
< double
char
< int
< long
< float
< double
这是两条线,主要在于 byte
类型不能自动转换为 char
类型。
当任何基本类型的值和字符串值进行连接运算时,基本类型的值将自动类型转换为字符串类型。因此,如果希望把基本类型的值转换为对应的字符串时,可以把基本类型的值和一个空字符串进行连接。+
不仅可以作为加法运算符,还可以作为字符串连接运算符使用。
|
|
强制类型转换
|
|
表达式类型的自动提升
byte
< short
< int
< long
< float
< double
char
< int
< long
< float
< double
与表数范围最大的类型保持一致。
流程控制
switch
switch
语句后面的控制表达式的数据类型是 byte
、short
、char
、int
4 种整数类型,枚举类型和 java.lang.String 类型(从 Java7 才允许),不能是 boolean
类型。
其他与 C 语言完全类似。
数组
Java 为工具类的命名习惯是添加一个字母 s,比如操作数组的工具类是 Arrays
,操作集合的工具类是 Collections
。
声明
数组在 Java 里是一种类型(引用类型)
|
|
初始化
只是声明数组还不能使用它,用 new
运算符创建数组
|
|
创建一个数组时:
byte
,short
,int
和long
这样的整数类型都被初始化为0
。boolean
数组的元素会初始化为false
。- 对象数组的元素则初始化为
null
,表示这些元素还未存放任何对象。
获得数组中元素的个数:
|
|
一旦创建了数组,就不能再改变其大小。
初始化的几种方法:
|
|
在 Java 中,允许数组长度为 0,在编写一个返回数组的方法时,这种语法形式就格外有用,可以创建一个长度为 0 的数组,new elementType[0]
for each
用来依次处理数组中的每个元素(其他类型的元素集合亦可),不必指定下标值。
|
|
collection
是一个数组或者是一个实现了 Iterable
接口的类对象(例如 ArrayList
)。
注意到循环变量是一个临时变量,系统会把数组元素依次赋给这个临时变量,但是这个临时变量不是数组元素,如果希望改变数组的元素的值,则不能使用 for each 这种方法。如果是想打印数组,可以利用 Arrays
类的 toString()
方法(注意使用这个方法要导入包 import java.util.Arrays
):
|
|
数组拷贝
在 Java 中,允许将一个数组用 =
拷贝给另一个数组,实际上,是这两个变量引用同一个数组:
|
|
如果想要把一个数组的所有值都拷贝到一个新数组中,就要用 Arrays
类的 copyOf()
方法。
在上面的代码中加入:
|
|
第二个参数是数组的长度,可以用这种方法初始化一个不同长度的数组。
|
|
运行效果如下:
|
|
面向对象
static
和 this
在静态方法中直接访问实例方法时会引发错误。如果真的需要在静态方法中使用普通的实例方法,则需要创建一个实例对象来调用实例方法。
|
|
报错:
|
|
大部分时候,普通方法访问其他方法、成员变量时无须使用 this
前缀,但如果方法里有个局部变量与成员变量同名,但程序有需要在该方法里访问这个被覆盖的成员变量,则必须使用 this
前缀。
方法
方法的参数传递机制
Java 里方法的参数传递方式只有一种——值传递(将实际参数值的副本传入方法内,而参数本身不会收到任何影响)。
引用变量传递的是一个对象的地址值。
形参个数可变的方法
在定义方法时,在最后一个形参的类型后增加三点 ...
,则表明该形参可以接受多个参数值,多个参数值被当成数组传入。
|
|
也可以用数组形参来定义方法。
|
|
这两种方法都包含了一个名为 books
的形参,在方法体内都可以把 books
当成数组处理,但是区别是调用两个方法时存在差别,对于以可变形参的形式定义的方法,调用方法更加简洁:
|
|
传给 books
参数的实参数值无须是一个数组,但如果采用数组形参来声明方法,调用时则必须传入一个数组。
|
|
明显第一种形式更加简洁,但是,数组形式的形参可以位于形参列表的任意位置,但个数可变的形参只能位于形参列表的最后。也就是说,一个方法中最多只能有一个长度可变的形参。
封装
对一个类或对象实现良好的封装,可以实现一下目的:
- 隐藏类的视线细节。
- 让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里加入控制逻辑,限制对成员变量的不合理访问。
- 可进行数据检查,从而有利于保证对象信息的完整性。
- 便于修改,提高代码的可维护性。
为了实现良好的封装,需要从两个方面考虑:
- 把对象的成员变量和实现细节隐藏起来,不允许外部直接访问。
- 把方法暴露出来,让方法来控制对这些成员变量进行安全的访问和操作。
特别注意:不要编写返回引用可变对象的 get 方法。这样会破坏封装性!
|
|
d
和 Harry.hireDay
引用同一个对象。d.setTime(...)
就自动地改变了这个雇员对象的私有状态。
使用访问控制符
3 个访问控制符:private
, protected
和 public
。
4 个访问控制级别(由小到大):private
, default
, protected
, public
。详细介绍:
private
:当前类访问权限,如果类里的一个成员(包括成员变量、方法和构造器等)使用private
访问控制符来修饰,则这个成员只能在当前类的内部被访问。用于修饰成员变量非常合适,可以将其隐藏在该类的内部。default
:包访问权限,类里的一个成员(包括成员变量、方法和构造器等不使用任何访问权限控制符修饰。default
访问控制的成员或外部类可以被相同包下的其他类访问。protected
:子类访问权限,该成员既可以被同一个包中的其他类访问,也可以被不同包中的子类访问。通常情况下,如果使用protected
来修饰一个方法,通常是希望其子类来重写这个方法。public
:即没有访问限制。
需要注意的是:访问控制符用于控制一个类的成员是否可以被其他类访问,对于局部变量而言,其作用域就是它所在的方法或代码块,不可能被其他类访问,因此不能使用访问控制符来修饰。
package
Java 引入了包 (package) 机制,提供了类的多层命名空间,用于解决类的命名冲突、类文件管理等问题。将一组功能相关的类放在同一个 package 下,从而组成逻辑上的类库单元。如果希望把一个类放在指定的包结构下,应该在 Java 源程序的第一个非注释行放置如下格式的代码:
|
|
一旦在 Java 源文件中使用了这个 package 语句,就以为着该源文件里定义的所有类都属于这个包。如果其他人需要使用该包下的类,也应该使用包名加类名的组合。
package 语句必须作为源文件的第一条非注释性语句,一个源文件最多只能包含一条 package 语句,该源文件中可以定义多个类,则这些类将全部位于该包下。
import
关键字
import
可以向某个 Java 文件中导入指定包层次下某个类或全部类,import
语句应该出现在 package
语句之后、类定义之前。
如果导入 vector.sub.Hello
类,应该使用如下代码:
|
使用 import
语句导入指定包下全部类:
|
|
其中 *
星号只能代表类,不能代表包,因此使用 import vector.*
语句时,它表明导入 vector
包下的所有类,但是如果 vector
包中有其他子包,这些子包中的类不会被导入,如需导入子包中的所有类,需要加上 import vector.sub.*
这样的语句。
Java 默认所有源文件导入 java.lang
包下的所有类,因此前面在 Java 程序中使用 String
, System
类时都无须使用 import
语句来导入这些类。
某些情况下只能在源文件中使用类全名。例如,需要在程序中使用 java.sql
包下的类,也需要使用 java.util
包下的类,则可以使用如下 import
语句:
|
如果接下来在程序中使用 Date
类,则会引起编译错误,因为上述两个包中都有这个类,而系统会不知道究竟要用哪个类,为了使引用更加明确,使用该类的全名:
|
|
静态导入
导入指定类的单个静态成员变量、方法:
|
|
导入指定类的全部静态成员变量、方法:
|
|
用 import
可以省略写包名;而使用 import static
则可以连类名都省略。
深入构造器
使用 this
调用另一个重载的构造器只能在构造器中使用,而且必须作为构造器执行体的第一条语句。使用 this
调用重载的构造器时,系统会根据 this
后括号里的实参来调用形参列表与之对应的构造器。
继承
Java 只能有一个直接父类,摒弃了 C++ 中难以理解的多继承特征。
重写父类的方法
子类包含与父类同名方法的现象被称为方法重写,也被称为方法覆盖。遵循“两同两小一大”规则,覆盖方法和被覆盖方法要么都是静态方法,要么都是实例方法。
super
限定
如果需要在子类方法中调用父类中被覆盖的方法,则可以使用 super
作为调用者来调用父类中被覆盖的方法。
当程序创建一个子类对象时,系统不仅会为该类中定义的实例变量分配内存,也会为它从父类继承得到的所有实例变量分配内存,即使子类定义了与父类同名的实例变量。也就是说,当系统创建一个 Java 对象,如果该 Java 类有两个父类(一个直接父类 A,一个间接父类 B),假设 A 类中定义了 2 个实例变量,B 类中定义了 3 个实例变量,当前类中定义了 2 个实例变量,那么这个 Java 对象会保存 2+3+2 个实例变量。
由上,个人认为如无必要,尽量使子类应用父类的成员变量,避免内存的浪费。
调用父类构造器
子类不会获得父类的构造器,但子类构造器可以调用父类构造器的初始化代码。
在一个构造器中调用另一个重载的构造器使用 this
调用来完成,在子类构造器中调用父类构造器使用 super
调用来完成。使用 super
调用父类构造器也必须出现在子类构造器执行体的第一行,所以 this
和 super
调用不会同时出现。
多态
仅针对方法而言。
编译类型为父类,运行类型为子类的对象,调用方法时,调用的是子类中重写父类的方法。这就会出现类型相同,而调用同一方法出现不同的行为特征,即多态。实例变量不具备多态性,故依然使用父类的实例变量。如下示例:
|
|
继承与组合
使用父类的注意要点
设计父类
为了保证父类有良好的封装性,不被子类随意更改,应遵循以下规则:
尽量隐藏父类的内部数据
尽量把父类的所有成员都设计成 private
类型,不要让子类直接访问父类的成员变量。
不要让子类可以随意访问父类的方法
父类中那些仅为辅助其他的工具方法,应该使用 private
访问控制符修饰。但又不希望子类重写该方法,可以使用 final
修饰符。如果希望父类的某个方法被子类重写,但是不希望被其他类自由访问,则可以使用 protected
来修饰该方法。
尽量不要在父类构造器中调用要被子类重写的方法
|
|
当系统试图创建 sub
对象时,同样会先执行其父类构造器,如果父类构造器调用了被其子类重写的方法,则变成调用被子类重写之后的方法。如上例,Base
构造器调用了
2 号 test()
方法,但是此时 Sub
对象的 name
实例变量是 null
,因此将引发空指针异常。
构建子类的必要条件
- 子类需要额外添加属性,而不仅仅是属性值的改变。
- 子类需要添加自己独有的行为方式(包括添加新的方法或重写父类的方法)。
如果只是出于子类复用的目的,并不一定需要使用继承,完全可以使用组合来实现。
利用组合实现复用
初始化块
初始化块是构造器的补充,初始化块总是在构造器执行之前执行。它是一段固定执行的代码,它不能接收任何参数。因此初始化块对同一个类的所有对象所进行的初始化处理完全相同。
如果有一段初始化处理代码对所有对象完全相同,且无须接收任何参数,就可以把这段初始化处理代码提取到初始化块中。
其实初始化块是一个假象,使用 javac
命令编译 Java 类后,该 Java 类中的初始化块会被还原到每个构造器中,且位于构造器中所有代码的前面。
静态初始化块
使用了 static
修饰符,在类初始化阶段执行静态初始化块,而不是在创建对象时才执行。
静态初始化块也属于类的静态成员——同样需要遵循静态成员不能访问非静态成员的规则,因此静态初始化块也不能访问非静态成员——实例变量和实例方法。
包装类
在 JDK 1.5 之后,提供了自动装箱和自动拆箱功能。
|
|
除此之外,包装类还可实现基本类型变量和字符串之间的转换。把字符串类型的值转换为基本类型的值有两种方式:
parseXxx(String s)
Xxx(String s)
|
|
如果希望把基本类型变量转换成字符串,有一种很简单的方式:
|
|
虽然包装类型的变量是引用数据类型,但包装类的实例可以与数值类型的值进行比较。
|
|
两个包装类的实例进行比较的情况就比较复杂,因为包装类的实例实际上是引用类型,只有两个包装类引用指向同一个对象时才会返回 true
。
|
|
自动装箱的特别情形:
|
|
为什么如果是两个 2 自动装箱后就想等,而如果是两个 128 自动装箱后就不相等呢?
这与 Java 的 Integer
类的设计有关,查看 Java 系统中 java.lang.Integer
类的源代码,如下所示:
|
|
如果以后把一个 -128 ~ 127 之间的整数自动装箱成一个 Integer
实例时,实际上是直接指向对应的数组元素,因此 -128 ~ 127 之间的同一个整数自动装箱成 Integer
实例时,永远都是引用 cache
数组的同一个数组元素,所以它们全部相等;但每次把一个不在 -128~127 范围内的整数自动装箱成 Integer
实例时,系统总是重新创建一个 Integer
实例,所以出现程序中的运行结果。
缓存是一种优秀的设计模式,把一些创建成本大、需要频繁使用的对象缓存起来,从而提高程序的运行性能。
处理对象
toString()
是 Object
类里的一个实例方法,所有的 Java 对象都具有 toString()
方法。
Object
类提供的 toString()
方法总是返回该对象实现类的 “类名+@+hashCode” 值,如果用户需要自定义类能实现“自我描述”的功能,就必须重写 Object
类的 toString()
方法。
==
和 equals()
使用 ==
使用 ==
来判断两个变量是否相等时,如果两个变量是基本类型变量,且都是数值类型(不一定要求数据类型严格相同),则只要两个变量的值想等,就将返回 true
。但对于两个引用类型变量,只有它们指向同一个对象时,==
判断才会返回 true
,==
不可用于比较类型上没有父子关系的两个对象。
|
|
常量池 constant pool
JVM 常量池保证相同的字符串直接量只有一个,不会产生多个副本。
使用 new String()
创建的字符串对象是运行时创建出来的,被保存在运行时内存区(堆内存)。
|
|
equals()
方法
有时程序在判断两个引用变量是否想等时,也希望有一种类似于“值相等”的判断规则,并不严格要求两个引用变量指向同一个对象。
equals()
是 Object
类提供的一个实例方法,因此所有引用变量都可调用该方法来判断是否与其他引用变量想等。但使用这个方法判断两个对象想等的标准与使用 ==
运算符没有区别,同样要求两个引用变量指向同一个对象才会返回 true
。因此这个 Object
类提供的 equals()
方法没有太大的实际意义。如果希望采用自定义的相等标准,则可采用重写 equals()
方法来实现。
String
已经重写了该方法,只要两个字符串中所包含的字符序列相同,通过 equals()
比较将返回 true
,否则将返回 false
。
|
|
通常而言,正确地重写 equals()
方法应该满足下列条件:
- 自反性:对任意
x
,x.equals(x)
一定返回true
。 - 对称性
- 传递性
- 一致性,对任意 x 和 y,如果对象中用于等价比较的信息没有改变,那么无论调用
x.equals(y)
多少次,返回的结果应该保持一致,要么一直是true
,要么一直是false
。 - 对于任何不是
null
的x
,x.equals(null)
一定返回false
。
类成员
static
修饰的成员就是类成员,static
关键字不能修饰构造器,static
修饰的类成员属于整个类,不属于单个实例。
对于 static
关键字而言,有一条非常重要的规则:类成员(包括方法、初始化块、内部类和枚举类)不能访问实例成员(包括成员变量、方法、初始化块、内部类和内部类)。因为类成员是属于类的,类成员的作用域比实例成员的作用域更大,完全可能出现类成员已经初始化完成,但实例成员还不曾初始化的情况,如果允许类成员访问实例成员将会引起大量错误。
如果一个类始终只能创建一个实例,则这个类被称为单例类。
在一些特殊情景下,要求不允许自由创建该类的对象,而只允许为该类创建一个对象。为了避免其他类自由创建该类的实例,应该把该类的构造器使用 private
修饰,从而把该类的所有构造器隐藏起来。
根据良好封装的原则:一旦把该类的构造器隐藏起来,就需要提供一个 public
方法作为该类的访问点,用于创建该类的对象,且该方法必须使用 static
修饰(因为调用该方法之前还不存在对象,因此调用该方法的不可能是对象,只能是类)。
|
|
通过 getInstance()
方法提供的自定义控制,保证 Singleton
类只能产生一个实例。所以在 main()
中,看到两次产生的 Singleton
对象实际上是同一个对象。
final
修饰符
final
关键字可用于修饰类、变量和方法,表明变量一旦获得了初始值就不可被改变。由于 final
变量获得初始值之后不能被重新赋值,因此 final
修饰成员变量和修饰局部变量时有一定的不同。
final
成员变量
对于 final
修饰的成员变量:如果既没有在定义成员变量时指定初始值,那么这些成员变量的值将一直是系统默认分配的 0
, \u0000
, false
或 null
,这些成员变量也就完全失去了存在的意义。因此 Java 语法规定:final
修饰的成员变量必须由程序员显式地指定初始值。与普通成员变量不同,系统不会对 final
进行隐式初始化,final
成员变量(包括实例变量和类变量)。
- 类变量:必须在静态初始化块中指定初始值或声明该类变量时指定初始值,而且只能在两个地方的其中之一指定。
- 实例变量:必须在非静态初始化块、声明该实例变量或构造器中指定初始值,而且只能在 3 个地方的其中之一指定。
|
|
final
局部变量
不能对 final
修饰的形参赋值。详见下例。
|
|
final
修饰引用类型变量
final
只保证这个引用类型变量所引用的地址不会改变,即一直引用同一个对象,但这个对象完全可以发生改变。
不管是类变量、实例变量还是局部变量,只要该变量满足 3 个条件,这个 final
变量就不再是一个变量,而是相当于一个直接量。
- 使用
final
修饰符修饰。 - 在定义该
final
变量时指定了初始值。 - 该初始量可以在编译时就被确定。
编译器会吧程序中所有用到该变量的地方直接替换成该变量的值。
final
方法
用 final
修饰的方法无法重写。
final
类
用 final
修饰的类不可以有子类,例如 java.lang.Math
。
不可变类
不可变 (immutable) 类的意思是,创建该类的实例后,该实例的实例变量是不可改变的。例如 Java 提供的 8 个包装类和 java.lang.String
类都是不可变类,这些类没有提供修改其实例变量的方法。
如果要创建自定义的不可变类,要遵守如下规则:
- 使用
private
和final
修饰符来修饰该类的成员变量。尤其要注意引用变量,因为如果只是用final
修饰引用变量,而没有用private
保护引用变量不被外。 - 提供带参数构造器,用于提供传入参数来初始化类里的成员变量。
- 仅为该类的成员变量提供
getter
方法,不提供setter
方法,因为普通方法无法修改final
修改的成员变量。 - 如果有必要,重写
Object
类的hashCode()
和equals()
方法。equals()
方法根据关键成员变量来作为两个对象是否相等的标准,除此之外,还应该保证两个用equals()
方法判断为相等的对象的hashCode()
也想等。
缓存实例的不可变类
用一个数组来作为缓存池,从而实现一个缓存实例的不可变类。
抽象类
抽象方法和抽象类
- 均须用
abstract
来修饰。抽象方法不能有方法体。 - 抽象类不能实例化,无法用
new
关键字调用抽象类的构造器创建抽象类的实例。 - 抽象类可以包含成员变量、方法(普通方法和抽象方法都可以)、构造器、初始化块、内部类(接口、枚举)5 种成分。抽象类的构造器不能用于创建实例,主要是用于被其子类调用。
- 含有抽象方法的类(包括直接定义了一个抽象方法;或继承了一个抽象父类,但没有完全实现父类包含的抽象方法;或实现了一个接口,但没有完全实现接口包含的抽象方法三种情况)只能被定义成抽象类。
使用抽象类和抽象方法的优势,可以更好地发挥多态的优势,使得程序更加灵活。详见下例:
|
|
|
|
|
|
由于在 Shape
类中定义了 calPerimeter()
方法和 getType()
方法,所以程序可以直接调用 triangle
变量和 circle
变量的这俩个方法,无须强制类型转换为其子类类型。
提供通用算法,但一些具体的实现细节则推迟到其子类 CarSpeedMeter
类中实现。这也是一种典型的模版模式。
使用模版模式的简单规则
- 抽象父类可以只定义需要使用的某些方法,把不能实现的部分抽象成抽象方法,留给其子类去实现。
- 父类中可能包含需要调用其他系列方法的方法,这些被调方法既可以由父类实现,也可以由其子类实现。父类里提供的方法只是定义了一个通用算法,其实现也许并不完全由自身实现,而必须依赖于子类的辅助。
以下是一些注意要点:
访问权限
abstract
关键字修饰的方法必须被其子类重写才有意义,否则这个方法将永远不会有方法体,因此 abstract
方法不能被定义为 private
访问权限。
成员变量与构造器
abstract
不能用于修饰成员变量,不能用于修饰局部变量,也不能用于修饰构造器。抽象类中定义的构造器也只能是普通构造器。
static
与 abstract
没有所谓的类抽象方法。因为使用 static
修饰一个方法时,可以通过该类调用该方法,但是如果该方法被定义成抽象方法,通过该类调用该方法会导致错误。
虽然 static
与 abstract
不能同时修饰某个方法,但是它们可以同时修饰内部类。
抽象方法和空方法体
注意区别:public abstract void test();
是一个抽象方法,它根本没有方法体,即方法定义后面没有一对大括号;但 public void test(){}
方法是一个普通方法,它已经定义了方法体,只是方法体为空,因此这个方法不可用 abstract
来修饰。
接口
接口中不能包含普通方法,接口中所有方法都是抽象方法。Java 8 对接口进行了改进,允许在接口中定义默认方法,默认方法可以提供方法实现。
接口的好处是让规范和实现分离。
接口的定义
基本语法
|
|
- 修饰符可以是
public
或者省略。 - 接口名与类名采用相同的命名规则。
- 一个接口可以有多个直接父接口,但接口只能继承接口,不能继承类。
- 注意:只有在 Java 8 以上的版本中才允许在接口中定义默认方法、类方法。
- 不管是否使用修饰符,接口里的普通方法总是使用
public abstract
来修饰。接口里的普通方法不能有方法实现(方法体);但类方法、默认方法都必须有方法实现。默认方法必须使用default
修饰。 - 在接口中定义成员变量时,不管是否使用
public static final
修饰符,接口里的成员变量总是使用这三个修饰符来修饰。而且接口里没有构造器和初始化块,因此接口里定义的成员变量只能在定义时指定默认值。 - 接口里定义的内部类、内部接口、内部枚举默认采用
public static
修饰,无论定义时是否这样指定。
如下例:
|
|
使用接口
TODO:
接口不能用于创建实例,但接口可以用于声明引用类型变量。当使用接口来声明引用类型变量时,这个引用类型变量必须引用到其实现类的对象。除此之外,接口的主要用途就是被实现类实现。归纳起来,接口主要有如下用途:
- 定义变量,也可用于进行强制类型转换。
- 调用接口中定义的常量。
- 被其他类实现。
一个类可以实现一个或多个接口,继承使用 extends
,实现使用 implements
关键字。这也是 Java 为单继承灵活性不足所做的补充。
|
|
接口和抽象类
接口
接口作为系统与外界交互的窗口,接口体现的是一种规范。
对于接口的实现者而言,接口规定了实现者必须向外提供哪些服务(以方法的形式来提供)。
对于接口的调用者而言,接口规定了调用者可以调用哪些服务,以及如何调用这些服务(就是如何来调用方法)。
当在一个程序中使用接口时,接口是多个模块间的耦合标准。
当在多个应用程序之间使用接口时,接口时多个程序之间的通信标准。
从某种程度上来看,接口类似于整个系统的“总纲”,它制定了系统各模块应该遵循的标准,因此一个系统中的接口不应该经常改变。一旦接口被改变,对整个系统甚至其他系统的影响将是辐射式的,导致系统中大部分类都需要改写。
抽象类
作为系统中多个子类的共同父类,它所体现的是一种模版式设计。抽象类作为多个子类的抽象父类,是已经实现了系统的部分功能(那些已经提供实现的方法),但这个产品依然不能当成最终产品,必须有更进一步的完善,这种完善可能有几种不同的方式。
内部类
主要作用:
- 内部类提供了更好的封装,将内部类隐藏在外部类中,不允许同一个包中的其他类访问该类。
- 内部类成员可以直接访问外部类的私有数据,因为内部类被当成其外部类成员,同一个类的成员之间可以互相访问。但外部类不能访问内部类的实现细节,例如内部类的成员变量。
- 匿名内部类适合用于创建那些仅需要使用一次的类。
注意:
- 内部类比外部类可以多使用三个修饰符:
private
,protected
,static
——外部类不可以使用这三个修饰符。 - 非静态内部类不能拥有静态成员。
分辨同名的外部类成员变量、内部类成员变量和内部类里方法的局部变量。
|
|
如果外部类需要访问非静态内部类的成员,则必须显示创建非静态内部类对象来访问。
如果存在一个非静态内部类对象,则一定存在一个被它寄生的外部类对象。但外部类对象存在时,外部类对象里不一定寄生了非静态内部类对象。
根据静态成员不能访问非静态成员的规则,不允许在外部类的静态成员变量中直接使用非静态内部类。
|
|
Java 不允许在非静态内部类有静态方法、静态成员变量、静态初始化块。
使用内部类
在外部类内部使用内部类
和普通的类没有什么区别,只是不要在外部类的静态成员中使用非静态内部类。因为静态成员不能访问非静态成员。
在外部类以外使用非静态内部类
在外部类以外的地方定义内部类(包括静态和非静态两种)变量的语法格式如下:
|
|
另外,如果外部类有包名,则还应该增加包名前缀。
局部内部类
很少使用。
如果把一个内部类放在方法里定义,这个内部类就是一个局部内部类,局部内部类仅在方法里有效,因此局部内部类不能使用访问控制符和 static
修饰符修饰。
Java 8 改进的匿名内部类
创建匿名内部类的格式如下:
|
|
匿名内部类必须继承一个父类,或实现一个接口,但最多只能继承一个父类,或者实现一个接口。
匿名内部类不能是抽象类,因为系统在创建匿名内部类时,会立即创建匿名内部类的对象。因此不允许将匿名内部类定义成抽象类。
匿名内部类不能定义构造器。由于匿名内部类没有类名,所以无法定义构造器,但匿名内部类可以定义初始化块,可以通过实例初始化块来完成构造器需要完成的事情。
最常用的创建匿名内部类的方式是需要创建某个接口类型的对象。
|
|
Product
只是一个接口,无法直接创建对象,因此此处考虑创建一个 Product
接口实现类的对象传入该方法。
当通过实现接口来创建匿名内部类时,匿名内部类也不能显示创建构造器,因此匿名内部类只有一个隐式的无参数构造器,故 new
接口名后的括号里不能传入参数值。
但如果通过继承父类来创建匿名内部类时,匿名内部类将拥有和父类相似的构造器,即拥有相同的形参列表。
TODO:
在 Java 8 之前,Java 要求被局部内部类、匿名内部类访问的局部变量必须使用 final
修饰,从 Java 8 开始这个限制被取消了,Java 8 智能:如果局部变量被匿名内部类访问,那么该局部变量相当于自动使用了 final
修饰。
Java 8 将这个功能称为 “effectively final”。
参考
《疯狂 Java 讲义》第三版