1、类
1.1 面向对象
面向对象三大特性
- 封装:客观事物抽象并封装成对象,将数据成员、属性和方法等集合在一个整体内;
- 继承:用于代码重用;
- 多态:同样的消息被不同类型的对象接收时导致完全不同的行为。
1.2 类和对象概述
1.2.1 声明
声明形式
[类修饰符] class 类名 [extends 基类] [implements 接口列表]{
类体
}[;]
说明
No | 说明 |
---|---|
1 | [类修饰符] 可选,可为 public, abstract| final, strictfp |
2 | class 关键字,注意首字母小写 |
3 | [extends基类] 可选,继承 |
4 | [implements接口列表] 可选:接口 |
5 | [;] 可选 |
可访问性:公共类与友好类
No | 说明 |
---|---|
1 | 声明类时使用修饰符 public 修饰的类为公共类,公共类可以被所有其他类访问,可被同一包中的类直接引用 |
2 | 声明类时使用默认修饰符而没有 public 的类为友好类,友好类只能被同一包中的类访问 |
1.2.2 创建和使用对象
创建对象的语法:
类名 对象名 = new 类名( [参数表] );
类名 对象名; 对象名 = new 类名( [参数表] );
说明:
No | 说明 |
---|---|
1 | 访问要使用点 (.) 运算符,是否可访问受访问修饰符的限制 |
2 | Java 中所有的类都是在堆中创建的 |
对象的比较
用 new
创建一个类对象将在托管堆中为对象分配一块内存,每个对象有不同的内存,代表对象的变量存储的是存放对象的内存的地址,因此即使两个不同的对象的内容相同,它们也是不相等的。但是,如果将一个对象赋值给另一个对象,那么,它们的变量都保存同一块内存的地址,即两个对象相同,且改变一个对象会影响另一个对象。
1.3 类的成员
1.3.1 静态成员与实例成员
静态成员,特征:
- 一般通过类名进行引用:
类名.静态字段名
,类名.静态方法名
,用对象访问时会出错; - 静态字段共享存储位置,无论创建多少实例,只有一个副本;
- 静态函数成员属于整个类,故在其代码体内不能直接引用实例成员。
实例成员,特征:
- 实例成员必须通过对象实例引用:
对象.实例字段名
,对象.方法名
; - 实例字段属于类的实例,创建一个对象即分配一块内存区域;
- 实例函数成员作用于类的实例,故其方法体内既可使用实例成员,亦可使用静态成员。
this 关键字:
- 如果定义的局部变量和实例字段重名,可以使用 this 关键字引用类的实例字段。方式:
this.实例字段
- 不能在静态方法中使用 this 关键字
初始化顺序:
- 在类内部,变量定义顺的先后决定了初始化的顺序,即使变量定义散布于方法之间,它们仍会在任何方法(包括构造器方法)被调用之前得到初始化;
- 静态字段只有在对象被创建或者第一次访问静态数据的时候,它们才会被初始化,而且初始化之后就不会被再次初始化,静态字段的初始化要先于实例字段的初始化;
- 静态初始化代码块和实例代码块的初始化过程与相应的字段的条件相同;
- 初始化的构造函数时,先调用最顶层基类的构造函数,逐级向下直到本类的构造函数
示例程序:
// 这里会被最先调用,但是如果我们不加new TestClass()这段,而仅仅声明一个空的testClass,
// 那么这里也依然不会被调用
static TestClass testClass = new TestClass();
public static void main(String ...args) {
TestClass testClass = new TestClass();
}
private static class TestClass {
private int val0 = say(0);
private static int sVal0 = staticSay(0);
static { System.out.println("静态初始化代码块"); }
{ System.out.println("实例初始化代码块"); }
public TestClass() {
System.out.println("Constructor()");
}
private int val1 = say(1);
private int say(int i) {
System.out.println("say(" + i + ")");
return 0;
}
private static int staticSay(int i) {
System.out.println("static say(" + i + ")");
return 0;
}
private int val2 = say(2);
private static int sVal1 = staticSay(1);
}
输出结果:
static say(0)
静态初始化代码块
static say(1)
say(0)
实例初始化代码块
say(1)
say(2)
Constructor()
----------
say(0)
实例初始化代码块
say(1)
say(2)
Constructor()
1.3.2 字段
声明:
[字段修饰符] 类型 字段名 [ = 初值 [, 字段名[ = 初值] ... ]];
字段修饰符可选: public | protected | private , static , final , transient , volatile
访问:
对象.实例字段名;
静态字段和实例字段:
静态字段是使用 static 修饰符声明的字段,实例字段是未使用 static 关键字声明的字段。
声明:
[字段修饰符] static 类型 字段名 [ = 初值 [, 字段名[ = 初值] ... ]];
访问:
类名.静态字段名;
常量字段:
[修饰符] final 字段名 [ =初值 [, 字段名 [, =初值] ... ] ];
常量只能被赋值一次,否则会出现编译错误. 静态常量在定义的时候就应该被初始化,实例常量定义时可以不初始化,但是应该在构造方法中初始化。
volatile 字段和 transient 字段:
前一个字段用在多线程中,保证了不同线程对这个变量进行操作时的可见性;后一个用在序列化中,表示该字段不用被序列化。
1.3.3 方法
方法的声明和调用:
[方法修饰符] 返回值类型 方法名 ( [ 形参列表 ] ){
方法体;
}[;]
- 方法修饰符可以是
public | protected | private, abstract | final, static, synchronized, native, strictfp
; - 使用 native 修饰符修饰的方法叫做本地方法,就是指用本地程序设计语言(如 C 或者 C++)来编写的特殊方法。
方法的调用方式
对象.方法名( [参数列表] );
参数的传递:
-
按值传递
- 一个方法不能修改基本数据类型的参数;
- 一个方法可以改变一个对象参数的状态;
- 一个方法不能让对象参数引用另一新的对象;
- 当传入的是基本数据类型的时候,无法修改基本数据类型的值,而对象类型引用的值是可以修改的。
-
可变形参
- 一个方法最多允许一个可变形参,且可变形参只能为方法的最后一个参数.;
- 可变形参允许向方法传递可变数量的实参(0 或多个)。
方法的重载:
- 方法的签名是由方法的名称及其参数的数目和类型组成(注意不包含返回类型);
- 重载就是相同名称的,但是参数的类型或者数目不同的不同方法之间构成重载关系。
- 对重载方法,如果传入的实际参数比较大,就将其窄化处理(强制转换),否则编译器会报错;如果传入的实际参数比较小,就将其提升为,比如 byte 类型可以传给 int 参数等。
静态方法和实例方法:
- 修饰符:静态方法使用 staitc 修饰,实例方法不使用 staitc 修饰;
- 访问方式和范围:静态方法属于整个类,只能通过类名访问:
类名.静态方法名
;实例方法属于某个实例,可以通过 对象名:实例方法名 进行访问
; - 权限:静态方法只能直接访问静态成员;实例方法能够访问静态成员和实例成员。
1.4 对象构造
1.4.1 构造方法
执行类的实例化工作,没有显式声明构造方法时会自动生成一个默认的无参构造方法,并将未赋值的字段设置为默认值。但是如果声明了带参数的构造方法,则编译器不会自动生成默认的无参构造方法,若需要使用无参构造方法则需要显示地声明。
也就是说,如果类中提供了至少一个构造器,但没有提供无参的构造器,则使用无参构造器构造对象就是违法的。而如果没有定义任何构造器,系统会提供一个无参构造器。
构造方法声明的形式
[ 修饰符 ] 类名 ( [参数列表] ){
构造方法体;
}[;]
- 修饰符可选,可为
public|protected|private
; - 类名与构造方法名相同;
- 注意构造方法无返回值类型,即使 void 也不行;
- 一般构造方法总是 public 的,private 类型的构造方法表明类不能被实例化,常用于只含有静态成员的类;
- 不能显式调用构造方法;
- 构造方法中,一般不要做初始化以外的事情;
- 可以在一个构造器中调用另一个构造器(构造器之间满足重载关系),方法是使用this指针,即在一个构造器中使用
this(...)
的形式就可以调用俩一个构造器; - 如果一个类是另一个类的子类,那么使用
super(...)
可以调用父类的构造方法,且该语句必须是子类构造器的第一条语句; - 还可以使用工厂方法构造实例;
- 遇到多个构造参数时考虑使用构建者模式(请参考设计模式部分)
1.4.2 初始化代码块
静态初始化代码块:
static {
静态初始化代码块;
}[;]
- 建议一个类只包含一个实例化和静态初始化代码块;
- 初始化代码块用于初始化类所需的操作;
- 在创建一个实例或引用任何静态成员之前,将自动调用静态初始化代码块,执行静态初始化;在创建一个实例时,将自动调用实例化初始化代码块。类的静态初始化代码块在程序中至多执行一次,而初始化代码块创建一个对象时执行一次。
实例化代码块:
{
实例化代码块
}[;]
实例化代码块只要构造类的对象,这些块就会被调用。
“析构”关键字 finalize:
在 C++ 中,对每个函数除了有构造函数还有一个析构函数,在 Java 中是没有析构函数的,不过可以在一个方法的前面添加 finalize 关键字赋予一个方法析构的功能。即添加 finalize 关键字的方法会在垃圾回收器清除对象之前调用,但尽量不要使用它,因为无法确定它何时会被调用。
1.7 类成员访问修饰符
修饰符 | 同一个类 | 同一包 | 不同包,子类 | 不同包,非子类 |
---|---|---|---|---|
public | Yes | Yes | Yes | Yes |
protected | Yes | Yes | Yes | |
默认 缺省 | Yes | Yes | ||
private | Yes |
- 如果我们希望一个类成员只由该类的子类使用,那么我们将其定义为 protected 类型;
- 如果我们希望一个类成员只在该类的内部使用,那么我们将其定义为 private 类型;
- 如果我们希望一个类成员可由任何类访问,那么我们将其定义为 public 类型;
- 如果我们希望一个类成员只在该类所处的包中使用,那么我们将其定义为默认的类型。
1.8 嵌套类
- 在一个类中定义的类. 嵌套类分为嵌套顶级类和内部类. 内部类又分实例内部类、本地内部类和匿名内部类;
- 在内部类(非静态)中访问外部类的成员可以使用
外部类名.this.成员名字
的形式。
嵌套顶级类:
-
关键字 static 修饰的内部类;
-
嵌套顶级类属于类的静态成员,与具体的对象实例无关;
-
嵌套顶级类的成员方法可以直接访问其顶级类的所有静态成员,但不能访问顶级类的非静态成员;
-
声明并创建嵌套顶级类的方法:
顶级类名.嵌套顶级类名 嵌套顶级类对象 = new 顶级类对象.嵌套顶级类名();
实例内部类:
-
声明时前面没有加static关键字修饰的嵌套类为实例内部类;
-
实例内部类的成员方法可访问其顶级类的所有成员.(包括private成员);
-
声明并创建实例内部类的方法:
顶级类名.实例内部类名 实例内部类对象 = new 顶级类对象.实例内部类名();
本地内部类:
- 在一个类的方法中定义的类为本地内部类,本地内部类可以访问顶级类的静态成员和实例成员外,还可以访问其所在的方法中的局部变量和方法的参数;
- 本地内部类不能使用 private 和 public 等修饰,作用域局限于局部类的块中;
- 如果在本地内部类中访问外部类或者方法的变量成员,则必须将被访问的变量成员定义为 final 的。
匿名内部类:
匿名内部类没有显式的类名,声明的同时,必须使用 new 关键字创建其对象实例。
1.10 对象的生命周期
1.10.1 对象的创建
使用 new 关键字创建对象,创建对象的方法:
- 使用 new 创建对象,如
String s = new String(“abc”);
; - 使用反射创建对象,如
Object s2 = Class.forName(“String”);
; - 调用对象的
clone()
方法,这种方法不会调用构造函数; - 使用反序列化方法;
- 隐式创建,如
String s3 = ”abc”; String s4 = s3 + “xyz”;
。
1.10.2 对象的使用
1.10.3 对象的销毁
Java语言使用垃圾回收器 (GC) 机制来销毁对象. 可以通过调用 System.gc()
强制运行垃圾回收器.
2、继承和多态
2.1 继承概述
- 将超类赋值给子类之前应该先进行
instanceof
检查,但应尽量少用,因为这会带了额外的开销; - 将子类的引用赋值给超类变量,编译器是允许的;
- 将超类的引用赋值给子类变量,必须进行强制类型转换;
- 被final修饰的类无法被继承,类中的方法也会默认为
final
的,即无法被子类覆写。
2.2 继承
2.2.1 派生类的声明
派生类使用 extends 关键字指定要继承的基类:
[ 类修饰符 ] class 类名 [ extends 基类 ] {
类体;
}
注:Java不支持多重继承。
2.2.2 super 关键字
使用 super 关键字从派生类访问基类的成员:
- 指定创建派生类实例时应调用的基类构造方法:
super(参数)
; - 调用基类上已经被其他方法重写的方法:
super.方法(参数)
; - 访问基类的数据成员:
super.字段名
。
注:不能在静态方法中使用 super 关键字.
2.2.3 构造方法的继承和调用
如果使用派生类的其他构造方法构造对象实例,则必须在派生类的构造方法的第一条语句中,显式地使用 super 关键字调用其基类的构造方法,否则会编译错误。
2.2.4 类成员变量的隐藏
如果派生类中声明了与继承的成员变量名同名的成员变量,则该重名成员变量将隐藏从基类继承的同名成员变量,成为成员变量的隐藏。若需要引用从基类中继承的同名成员变量,可使用 super 关键字. 即:super.变量名
。
2.2.5 类方法的重写
- 全新方法:与基类方法的 方法名和参数列表(参数类型或者个数)均不同;
- 重载方法:与基类方法的 方法名相同,但是参数列表(参数类型或者个数)不同;
- 重写方法:与基类方法的 方法名和参数列表(参数类型或者个数)均相同。
注意:
- 当基类的方法被重写,而又要在派生类中调用基类的方法时,可以使用super关键字:
super.方法名
; - 虽然返回类型不是方法签名的一部分,但是派生类中重写的方法的返回类型必须与基类中被重写的方法相同;
- 派生类中重写的方法不能缩小基类中被重写方法的访问权限,权限顺序:
public protected private
; - 基类的非静态方法不能被派生类的静态方法重写,基类的静态方法不能被派生类的非静态方法重写;
- 基类的final方法不能被派生类重写;
- 基类的abstract方法必须被派生类重写,否则派生类也必须为
abstract
。
2.2.6 抽象类和抽象方法
抽象类:
- 使用
abstract
修饰的类为抽象类; - 抽象类不能被实例化,对抽象类使用
new
关键字会导致错误. 但是可以将子类型的实例赋值给抽象的超类,比如
Person p = new Student(“Name”,id);
其中 Person 是超类,Student 是继承了 Person 的子类; - 允许(但不要求)抽象类包含抽象成员. 也就是抽象类可以包含具体数据和具体方法,甚至不包含抽象方法;
- 抽象类不能被密封,即不能被
final
修饰; - 当从抽象类派生非抽象类时,非抽象类必须实现所继承的所有抽象成员.否则子类必须为抽象的。
抽象方法:
- 使用
abstract
关键字修饰的方法; - 抽象方法不提供该方法的任何实际实现;
- 除构造方法、静态方法和私有方法不能声明为抽象的外,其他任何方法均可声明为抽象方法;
- 如果派生类从基类继承了抽象方法,则要么重写以实现所有抽象方法,要么使用关键字
abstract
声明派生类为抽象类。
2.2.7 密封类和密封方法
密封类:
- 将关键字 final 置于 class 前面即可将类声明为密封类;
- 密封类不能作为其他类的基类,因此它不能是抽象的。
密封方法:
- 使用 final 修饰的方法,也称为最终方法;
- 使用 final 修饰可以防止派生类进一步重写该方法;
- 可将密封类和抽象类、密封方法和抽象方法看作对立的,即:抽象的类必须被继承,抽象的方法必须被重写;密封的类禁止被继承,密封的方法禁止被重写。
2.3 多态
实现多态有两种方法:
- 方法重载(静态绑定):编译时确定;
- 方法重写(动态绑定):运行时确定。
只有普通的方法是多态的,字段不是多态的:
public static void main(String ...args) {
Sub sub = new Sub();
System.out.println("sub.field = " + sub.filed);
System.out.println("((Super) sub).filed = " + ((Super) sub).filed);
System.out.println("sub.getField() = " + sub.getFiled());
System.out.println("((Super) sub).getFiled() = " + ((Super) sub).getFiled());
System.out.println("sub.getSuperField() = " + sub.getSuperField());
}
private static class Super {
public int filed = 0;
public int getFiled() {
return filed;
}
}
private static class Sub extends Super {
public int filed = 1;
@Override
public int getFiled() {
return filed;
}
public int getSuperField() {
return super.filed;
}
}
输出结果:
sub.field = 1
((Super) sub).filed = 0
sub.getField() = 1
((Super) sub).getFiled() = 1
sub.getSuperField() = 0
这里为 Super.field 和 Sub.field 分配了不同的存储空间,在 Sub 中实际上包含了两个名为 filed 的域:它自己定义的和基类中的。引用 Sub 中的 filed 时默认会引用到 Sub.field,如果要访问 Super.field,必须显式地使用 super.field.
3、接口
- 接口本身不提供任何方法的实现;
- 继承接口的任何非抽象类都必须实现接口的所有成员;
- 接口类似于抽象基类,不能被实例化.接口不是类,不能用 new 实例化;
- 接口中所有方法默认为 public 和 abstract(所有方法都是公共抽象的);
- 接口可以包含抽象方法和静态常量字段;
- Java 不支持多重继承,但是类和结构可以从多个接口继承. 接口本身可从多个接口继承。
3.1 接口的声明
接口声明
[ 接口修饰符 ] interface 接口名 [ extends 基接口列表 ] {
接口体;
}[;]
3.2 接口成员
- 接口只能包含抽象方法和静态常量字段;
- 接口成员变量默认为静态常量字段 (
public static final
),但接口成员变量不能带除public static final
以外任何修饰符; - 接口成员方法默认为公共抽象方法 (
public abstract
),但接口成员方法不能带除public abstract
以外任何修饰符。
3.3 接口实现
派生类使用 implements 关键字指定要实现的基接口列表:
[ 类修饰符 ] class 类名 [ implements 基接口列表 ]{
类体;
}
3.4 接口继承
接口从零或多个接口继承,被继承的接口称为该接口的基接口. 接口使用关键字 extends 指定要继承的接口。故实现该接口的类必须实现接口本身以及该接口的基类的成员:
[ 接口修饰符 ] interface [ extends 基接口列表 ]{
类体;
}