一、类和对象
类(class)是某一批对象的抽象,可以被认为是一种自定义的数据类型。可以使用类来定义变量,所有使用类定义的变量都是引用变量,它们将会引用到类的对象。
对象(instance)是一个具体存在的实体。
类是面向对象的核心,面向对象的三大特征:封装、继承、多态。
类的成分包括:
- 成员变量:用于定义该类或该类的实例所包含的状态数据。
- 构造器:用于构造该类的实例。
- 初始化块:用于对java对象进行初始化操作。
- 方法:用于定义该类或该类对象的实例的行为特征或者功能特征。
- 内部类:提供更好的封装性。
1.1 成员变量
[修饰符] 类型 成员变量名 [= 默认值];
- 修饰符:可以省略,也可以是public、protected、private、static、final、。其中public、protected、private最多只能出现其中之一;可以和static、final组合起来修饰成员变量。
- 类型:可以是任何数据类型,包括基本数据类型和引用数据类型。
- 成员变量名:只要是一个合法的标识符即可,小驼峰命名规则。
被static修饰的变量,称为静态变量,或类变量。
没有被static修饰的变量,称为非静态变量,或实例变量。
1.2 成员方法
被static修饰的方法,称为静态方法,或类方法。
没有被static修饰的方法,称为非静态方法,或实例方法。
1.2.1 方法的定义
[修饰符] 方法返回值类型 方法名 (形参列表){
// 方法体
}
- 修饰符:可以省略,也可以是public、protected、private、static、final、abstract。其中public、protected、private最多只能出现其中之一;final、abstract最多只能出现其中之一;它们可以和static组合起来修饰方法。
- 返回值类型:可以返回任何数据类型,且必须有一个有效的return语句;或使用void来声明没有返回值。
- 方法名:小驼峰命名规则,建议以英文动词开头。
- 形参列表:任意个数,由“形参类型 形参名”组成。
1.2.2 值传递
Java中的参数传递只有一种:值传递。就是将实际参数值的副本(复制品)传入方法内,而参数本身不会受到影响。
无论是基本数据类型,还是引用数据类型,都是采用值传递的方法。但是,对于引用数据类型,可能会产生一些误会。
将引用数据类型的变量当作实参传入方法中时,由于引用变量存储的是引用对象的地址值,所以实参和形参都指向同一对象,所以在方法中操作的就是同一对象。这就会产生一种错觉:方法中传入的就是对象本身,而不是复制品。但这显然是错误的。
1.2.3 可变参数
Java允许定义形参个数可变的参数,从而允许为方法指定数量不确定的参数。只需在最后一个参数类型后增加三点(…),则表明该形参可以接受多个参数值。
个数可变的参数只能处于形参列表的最后,一个方法中只能包含一个可变的形参。个数可变的形参本质是一个数组类型的参数,因此即可以传入多个参数,又可以传入一个数组。
以下两方法效果完全一样,且不能构成重载,实际上是同一个方法。
public void test(int a, String... book){}
public void test(int a, String[] books){}
1.2.4 方法重载
Java允许同一个类中定义多个同名的方法,只要参数列表不同即可。
方法重载条件:
- 方法名必须完全相同。
- 形参列表必须不同。
- 其他部分(返回值类型、修饰符等)与方法重载没有任何关系。
当重载可变参数时,不会优先调用可变参数,而是优先寻找相应参数的普通方法,若没有则调用可变参数。如果想直接调用可变参数的方法,应传入数组参数。
public class Test {
public void test(){
System.out.println("无参数的方法");
}
public void test(String msg){
System.out.println("单参数的方法");
}
public void test(String... msg){
System.out.println("可变参数的方法");
}
public static void main(String[] args) {
Test t = new Test();
t.test(); // 无参数的方法
t.test("a"); // 单参数的方法
t.test("a","a"); // 可变参数的方法
t.test(new String[]{"a"}); // 可变参数的方法
}
}
1.3 构造器
构造器,也称构造方法,是在进行创建对象的时候必须调用的。如果一个类没有构造器,那么这个类就无法创建实例。
因此,Java提供了一个功能:如果程序员没有为一个类编写构造器,则系统会为该类提供一个默认的无参构造器。一旦程序员为一个类提供了构造器,则系统就不会为该类提供默认构造器。
1.3.1 构造器的定义
[修饰符] 构造器名 (形参列表){
// 构造器执行体
}
- 修饰符:可以省略,也可以是public、protected、private其中之一。
- 构造器名:必须和类名完全相同。
- 形参列表:和定义方法形参列表格式相同。
构造器不能定义返回值类型,也不能使用void声明构造器没有返回值。但是实际上,构造器是有返回值的。使用new来调用构造器时,返回的就是且总是该类的实例,所以无需定义返回值。而且不能在构造器中显示使用return来返回当前类的对象,因为构造器的返回值是隐式的。
1.3.2 构造器与方法
构造器也称构造方法,是一种特殊的方法。
构造器 | 方法 | |
---|---|---|
名称 | 必须与类名完全相同 | 无特殊要求 |
返回值 | 不能定义返回值类型 | 必须定义返回值类型,或无返回值void |
如果构造器加了返回值类型,将变成一个普通方法。
public class User {
public User() {
System.out.println("这是一个构造器");
}
public void User(){
System.out.println("这是一个名叫User的方法");
}
public static void main(String[] args) {
User user = new User();
user.User(); // 调用User()方法
}
}
1.4 初始化块
[修饰符] {
// 初始化块执行体
}
1.4.1 普通初始化块
Java使用构造器来对单个对象进行初始化操作。与构造器作用非常类似的是初始化块,它也可以对Java对象进行初始化操作。初始化块只是在创建Java对象时隐式执行,而且在构造器之前执行。
一个类中可以有多个初始化块,相同类型的初始化块之间的有顺序:先定义的先执行,后定义的后执行。
实际上,初始化块是一个假象,使用 javac 命令编译Java类后,该Java类中的初始化块会消失——初始化块中代码会被“
还原
”到每个构造器中,且位于构造器中所有代码的前面。
1.4.2 静态初始化块
初始化块的修饰符只能是static,使用static修饰的初始化块称为静态初始化块,或类初始化块。
静态代码块可以用于在执行类的方法之前进行静态资源的初始化操作。
静态初始化是类相关的,系统在类初始化阶段执行类初始化块,而不是在创建对象时才执行。因此,静态初始化块总是比普通初始化块先执行。
1.5 内部类
类被定义成一个独立的程序单元。有时候,也会把一个类放在另一个类的内部定义,这个定义在其他类内部的类被称为内部类(或嵌套类),包含内部类的类被称为外部类(或宿主类)。
内部类详解 转 :内部类
二、类的创建与初始化
-
类的准备阶段
为该类的静态成员
分配内存,并默认初始化。 -
类的初始化阶段
调用父类静态初始化块
,子类静态初始化块…… -
对象的初始化阶段
父类普通初始化块、构造器
,子类普通初始化块、构造器。
public class Person {
private String pMsg;
{
System.out.println("Pserson 普通代码块");
}
static {
System.out.println("Pserson 静态代码块");
}
public Person() {
System.out.println("Pserson 无参构造器");
}
}
public class Student extends Person{
private String sMsg;
{
System.out.println("Student 普通代码块");
}
static {
System.out.println("Student 静态代码块");
}
public Student() {
System.out.println("Student 无参构造器");
}
public static void main(String[] args) {
Student student = new Student();
}
}
结果为:
Pserson 静态代码块
Student 静态代码块
Pserson 普通代码块
Pserson 无参构造器
Student 普通代码块
Student 无参构造器
三、封装
3.1 理解封装
封装指的是将对象的状态信息隐藏在对象内部,不允许外部直接访问对象的信息,而是通过该类所提供的方法,来实现对内部信息的操作和访问。
实现良好的封装,需考虑两方面:
- 将对象的成员变量和实现细节隐藏起来,不允许外部直接访问。
- 把方法暴露出来,让方法来控制对这些成员变量进行安全的访问和操作。
封装实际上就两个含义:把该隐藏的隐藏,该暴露的暴露。通过私有化隐藏成员变量,暴露相对于的set、get方法。
3.2 访问控制符
- private(当前类访问权限):如果类的成员(成员变量、方法、构造器等)使用了private来修饰,则这个成员只能在当前类的内部访问。通常用来修饰成员变量。
- default(包访问权限):如果类的成员(成员变量、方法、构造器等)或者一个外部类不使用任何访问控制符修饰,就称它为包访问权限。default修饰的成员或外部类,可以被相同包下的其他类访问。
- protected(子类访问权限):如果类的成员(成员变量、方法、构造器等)使用了protected来修饰,则这个成员既可以被同一个包下的其他类访问,还可以被其子类访问。如果使用protected来修饰一个方法,通常是希望子类来重写。
- public(公共访问权限):如果类的成员(成员变量、方法、构造器等)或一个外部类使用了public来修饰,则这个成员或外部类可以被所有类访问。
private | default | protected | public | |
---|---|---|---|---|
同一类中 | 可访问 | 可访问 | 可访问 | 可访问 |
同一包中 | 可访问 | 可访问 | 可访问 | |
子类中 | 可访问 | 可访问 | ||
全局范围内 | 可访问 |
四、继承
4.1 继承概述
-
继承是实现代码复用的重要手段,通过extends关键字来实现。
-
继承是类和类之间的一种的关系。除此之外还有依赖、组合、聚合等。
-
Java中只有单继承,没有多继承。
-
所有类都默认直接或间接继承Object类。
-
子类不能继承父类的构造器。
-
子类可以继承父类的私有成员,只是不能直接访问。可以利用反射访问。
-
子类是不能继承父类的静态成员,因为它是属于分类的静态域的,只会加载一次。但是子类可以访问父类的静态成员,因为父类的静态成员是可共享的,共享并非继承。
4.2 this与super
4.2.1 this
this可以代表任何对象,但总是指向调用该方法的对象。根据this出现位置的不同,this作为对象的默认引用有两种情形:
- 构造器中因引用该构造器正在初始化的对象。
- 在方法中引用调用该方法的对象。
(1)用于构造器
this可以用于构造器中作为默认引用,由于构造器是直接使用new关键字来调用,而不是使用对象来调用,所有this在构造器中代表该构造器正在初始化的对象。
在构造器中访问其他成员变量或方法是,都可以省略this前缀。但是如果在构造器中有一个与成员变量同名的局部变量,有必须在构造器中访问这个被覆盖的成员变量,则必须使用this前缀。
(2)用于方法
this的最大作用就是让类中的一个方法,访问该类中的另一个方法或实例变量。当this出现在某方法中时,它代表的对象是不确定的,但它的类型是确定的:它代表的只能是当前类的实例。只有当这个方法被调用时,它所代表的对象才被确定下来:谁调用这个方法,this就代表谁。
对于static修饰的方法,可以直接使用该类来直接调用该方法。static修饰的方法中不能使用this引用,所以静态成员不能直接方法非静态成员。
this也可以作为方法的返回值。
4.2.2 super
super用于限定该对象调用它从父类继承得到的实例变量或方法。
super和this一样,不能出现在static修饰的方法中。因为static修饰的方法是属于类的,该方法的调用者可能是一个类,而不是对象,因而super限定也就失去了意义。
(1) 调用父类实例变量
- 如果子类中定义了与父类同名的实例变量,则子类实例变量会隐藏父类实例变量。
- 注意:不是完全覆盖,而是隐藏。因此系统在创建子类对象时,依然会为父类中定义的,被隐藏的变量分配内存空间。
在某个方法中访问名为a的实例变量,没有显示指定调用者,则系统查找a的顺序为:
- 查找该方法中是否有名为a的局部变量。
- 查找当前类是否有名为a的成员变量。
- 查找当前类的直接父类是否包含名为a的成员变量,依次上溯所有父类,直到Object类。
public class Person {
public int a = 1;
}
public class Student extends Person{
public int a = 2;
public void printA(){
int a = 3;
System.out.println(a); // 3
System.out.println(this.a); // 2
System.out.println(super.a); // 1
}
public static void main(String[] args) {
new Student().printA();
}
}
(2) 调用父类构造器
子类不会获得父类的构造器,但是子类构造器中可以调用父类的构造器,类似于一个构造器调用另一个重载的构造器。
this和super调用构造器时,都必须出现在构造器的第一行,所有this调用和super调用不会同时出现。
当调用子类构造器来初始化对象时,父类构造器总会在子类构造器之前执行;不仅如此,执行父类构造器时,系统会再次上溯执行其父类构造器……依次类推,创建任何对象时,最先执行的总是Object类的构造器。
4.3 方法的重写
子类包含与父类同名的方法,称为方法重写,或方法覆盖。且只能重写非静态方法。
方法重写需要遵循以下规则:
- 方法名相同,形参列表相同。
- 子类方法返回值类型更小或相同。
- 子类方法声明抛出的异常更小或相同。
- 子类方法的访问权限更大或相同。
子类覆盖了父类方法后,子类的对象无法访问父类中被覆盖的方法,但是可以在子类方法中通过super调用父类中被覆盖的方法。
4.4 继承与组合
继承是实现类复用的重要手段,但是继承会破环封装。
组合也是实现类复用的重要方式,相比继承,组合能提供更好的封装性。
4.4.1 使用继承的注意事项
继承带来的问题
- 继承带来了高度复用的同时,严重破环了父类的封装性。子类可以直接访问父类的成员变量(内部信息)和方法,从而造成子类和父类的严重耦合。
- 父类的实现细节对子类不再透明,子类可以访问父类的成员变量和方法,并可以改变父类方法的实现细节(方法重写),从而导致了子类可以恶意篡改父类的方法。
为了保证父类有良好的封装性,设计父类通常应遵循如下规则:
- 尽量隐藏父类的内部数据。尽量把父类的所有成员变量都设置成private,不要让子类直接访问。
- 不要让子类可以随意访问、修改父类的方法。父类中那些仅为辅助其他的工具方法,应该使用private修饰,让子类无法访问。如果父类的方法需要被外部调用,则必须使用public修饰,但又不希望子类重写,可以使用final修饰。如果希望父类中的方法被子类重写,但是又不希望被其他类自由访问,可以使用protected修饰。
- 尽量不要在父类构造器中调用将要被子类重写的方法。
4.4.2 利用组合实现复用
组合就是把旧类对象作为新类的成员变量组合起来,用以实现新类的功能。用户可以看到新类的方法,而看不到被组合对象的方法。
五、多态
5.1 多态性
- 编译时类型:由声明该类变量时使用的类型决定。如下面代码中的 person2 编译时类型是 Person。
- 运行时类型:由实际赋给该变量的对象决定。如下面代码中的 person2 运行时类型是 Student。
当编译时类型和运行时类型不一致时,就会出现多态。即相同类型的变量,调用同一个方法时,呈现出多种不同的行为特征。
多态存在的条件:
- 有继承或实现关系。
- 子类重写了父类的方法。
- 父类引用指向子类对象。
public class Person {
public void eat(){
System.out.println("在家吃饭");
}
}
public class Student extends Person{
@Override
public void eat() {
System.out.println("在学校吃饭");
}
public static void main(String[] args) {
Person person1 = new Person();
Person person2 = new Student();
person1.eat(); // 在家吃饭
person2.eat(); // 在学校吃饭
}
}
上述代码中的 person2 对象,编译时类型为 Person,运行时类型为 Student,编译时类型和运行时类型不一致,出现多态。两Person的实例 person1 和 person2 调用同一 eat() 方法,呈现出不同的行为特征。
多态的优势:
- 在多态形式下,右边对象可以实现组件化切换,业务功能也随之改变,便于扩展和维护。可以实现类与类之间的解耦。
- 父类类型作为方法形参,传递子类对象给方法,可以传入一切子类对象进行方法的调用,更能体现多态的扩展性与便利。
多态的劣势:
- 多态形式下,不能直接调用子类特有的方法
5.2 instanceof
作用:
用于判断前面对象是否是后面的类,或者其子类、实现类的实例。返回值取决于运行时类型。
使用条件:
两者的编译时类型需要有继承关系,否则编译报错。
instanceof和(type)是Java提供的两个运算符。在进行强制转换之前,通常先用instanceof判断是否可以进行转换,在使用(type)进行强制转换。