Java面向对象下
# 1. 方法重载
方法重载(Overloading)是Java中一种允许同一个类里面包含多个同名方法,但它们的参数列表必须不同的机制。参数列表的不同可以是参数的数量不同、参数类型不同或者参数的顺序不同。方法重载是实现多态的一种方式,它提高了方法的灵活性。
方法重载的规则
- 同一个类中:重载的方法必须在同一个类里面。
- 方法名相同:重载的方法名称必须相同。
- 参数列表不同:可以是参数数量不同,参数类型不同,或者参数顺序不同。
- 与返回类型无关:重载的方法可以有不同的返回类型,但仅返回类型不同不足以成为方法的重载。
- 与访问修饰符无关:方法的访问修饰符可以不同,访问修饰符的不同也不影响方法的重载。
下面是一个简单的例子,展示了如何在Java中使用方法重载:
class Example {
// 方法1:无参数
public void display() {
System.out.println("无参数的display方法");
}
// 方法2:一个整型参数
public void display(int a) {
System.out.println("整型参数的display方法: " + a);
}
// 方法3:两个整型参数
public void display(int a, int b) {
System.out.println("两个整型参数的display方法: " + a + ", " + b);
}
// 方法4:一个字符串参数
public void display(String a) {
System.out.println("字符串参数的display方法: " + a);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
方法重载的好处
- 提高了代码的可读性:通过方法重载,可以让方法名保持一致性,而通过参数列表的不同来区分不同的操作,这样使得代码更易于理解和维护。
- 增加了方法的灵活性:重载提供了多种方式来执行相同的操作,可以根据不同的场景选择最合适的方法。
# 2. 可变形参
可变形参(Varargs)是Java 5引入的一个特性,它允许方法接受任意数量参数的简化语法。通过使用可变形参,你可以用一个方法处理不定量的参数,而不需要为每种参数数量创建多个重载方法。使用可变形参可以使代码更简洁,更灵活。
可变形参的基本语法
方法的修饰符 方法的返回值类型 方法名(数据类型... 参数名) {
// 方法体
}
2
3
可变形参的特点和规则
- 参数数量可变:方法的参数数量可以是0个、1个或多个。
- 与重载方法构成关系:可变形参的方法可以与同名的其他方法构成重载关系。但当调用同名方法时,有确定参数列表的方法优先于可变形参的方法被调用。
- 等价于数组参数:在底层,可变形参实际上通过数组来实现的。因此,可变形参与方法中的数组类型参数在功能上是等价的。但是,为了避免歧义,一个方法内不能同时使用可变形参和同类型的数组形参。
- 形参位置:可变形参必须位于方法参数列表的最后。因为在调用方法时,可变形参可以接受多个参数,如果不放在最后,编译器将无法确定哪些参数属于可变形参。
- 单个方法中只能有一个:在一个方法的参数列表中,只能声明一个可变形参。如果声明多个,编译器将无法确定每个参数的边界,导致编译错误。
public class VarargsExample {
// 使用可变形参处理多个字符串参数
public static void printStrings(String... strings) {
for (String str : strings) {
System.out.println(str);
}
}
public static void main(String[] args) {
// 调用方法时可以传入任意数量的字符串参数
printStrings("Hello", "World", "Java", "Varargs");
printStrings(); // 也可以不传入任何参数
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
在这个示例中,printStrings
方法可以接受任意数量的String
参数,甚至可以不传入任何参数。当方法被调用时,传入的参数被处理成一个数组,方法内部通过遍历这个数组来处理每个参数。
总之,可变形参提供了一种非常灵活的处理不确定数量参数的方法,让你能写出更简洁、更通用的代码。
# 3. 参数传递机制-值传递
在Java中,方法参数的传递机制只有一种:值传递(pass by value)。这意味着当你调用一个方法时,实际传递给方法的是参数值的一个副本,而不是参数本身。该机制适用于基本数据类型和引用数据类型的参数,但它们的表现形式略有不同。
基本数据类型的值传递
当方法的参数是基本数据类型时(如int
、double
、char
等),传递的是变量值的副本。在方法内部对这个值进行修改,不会影响到原始数据。
public class ValuePassDemo {
public static void main(String[] args) {
int num = 10;
System.out.println("调用前的num值:" + num);
changeValue(num);
System.out.println("调用后的num值:" + num);
}
public static void changeValue(int value) {
value = 55;
System.out.println("方法中的value值:" + value);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
在上面的代码中,changeValue
方法内部的修改对main
方法中的num
变量没有任何影响,因为传递给方法的是num
的值的副本。
引用数据类型的值传递
当方法参数是引用数据类型时(如数组、对象等),传递的是对象在堆内存中的地址值的副本。因此,在方法内部对这个引用所指向的对象进行修改,会影响到原始对象。但是,如果尝试在方法中直接改变引用的指向,不会影响到原始引用。
public class ReferencePassDemo {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5};
System.out.println("调用前的arr第一个元素:" + arr[0]);
changeFirstElement(arr);
System.out.println("调用后的arr第一个元素:" + arr[0]);
}
public static void changeFirstElement(int[] array) {
array[0] = 99;
}
}
2
3
4
5
6
7
8
9
10
11
12
在这个示例中,changeFirstElement
方法成功地修改了数组的第一个元素。这是因为传递给方法的是数组在堆内存中的地址的副本,这个地址指向的是同一个数组对象。
总的来说,无论是基本数据类型还是引用数据类型的参数,在Java中都是通过值传递的方式。关键的区别在于,基本数据类型传递的是值的副本,引用数据类型传递的是地址值的副本。理解这一点对于编写正确、可预测的Java程序至关重要。
# 4. import
在Java中,import
语句用于将其他包中的类引入到当前工作空间中,使我们能够在代码中使用那些类而不需要每次都写出完整的包名。这不仅减少了代码的冗余,也使得代码更加易读。下面是关于import
语句的几个关键点的详细说明:
基本格式:
import
语句的基本格式是import 包名.类名;
。这允许你在代码中直接通过类名访问该类,而不是使用全路径名。导入整个包:如果你需要从同一个包中导入多个类,可以使用
import 包名.*;
语法。这会导入该包下的所有类,但不包括子包中的类。默认导入:
- Java自动导入
java.lang
包下的所有类,因此不需要显式导入String
、Math
等常用类。 - 同一个包内的类之间互相访问时,也无需导入。
- Java自动导入
导入子包:导入一个包并不会自动导入它的子包。每个包都是独立的,必须显式地导入每个需要的类或使用子包的完整路径。
避免命名冲突:如果两个不同的包中含有同名的类,那么就需要在使用时指定完整的类名(即包含包名的类名),以区分这两个类。这种情况下,不能仅通过
import
语句来解决命名冲突。导入规范:
- 通常建议具体导入需要的类,而不是使用
.*
导入整个包,这可以减少名称空间的污染,也更有利于代码的理解和维护。 - 在编写大型项目时,合理组织包结构,并明智地使用
import
语句,可以使代码结构更清晰,依赖关系更明确。
- 通常建议具体导入需要的类,而不是使用
通过使用import
语句,我们可以更方便地在Java程序中引用其他包中的类,从而提高代码的模块化和重用性。
# 5. 权限修饰符
Java中的权限修饰符是一种语言机制,用于限制对类、变量、方法和构造器的访问,从而实现封装和隐藏信息。Java定义了四种不同的访问级别,通过使用不同的权限修饰符来设定:
- private:最受限的访问级别,被修饰的成员只能在同一个类中被访问。
- 默认(缺省):只有当两个类位于同一个包中时,一个类中的默认访问成员才能被另一个类访问。
- protected:提供包内访问和跨包的子类访问。在同一包内的其他类或不同包的子类可以访问protected成员,但是不同包的非子类不能访问。
- public:提供了最广泛的访问权限,任何其他类都可以访问被public修饰的成员。
权限修饰符的具体应用范围如下表所示:
1 | 访问级别 | 访问控制修饰符 | 同类 | 同包 | 不同包子类 | 不同包 |
---|---|---|---|---|---|---|
2 | 公开 | public | 可以 | 可以 | 可以 | 可以 |
3 | 受保护的 | protected | 可以 | 可以 | 可以 | 不可以 |
4 | 默认 | default | 可以 | 可以 | 不可以 | 不可以 |
5 | 私有的 | private | 可以 | 不可以 | 不可以 | 不可以 |
# 6. 构造器(Constructor)
构造器(Constructor)是Java中一种特殊的方法,主要用于在创建对象时初始化对象,即为对象成员变量赋初始值。构造器具有与类相同的名称,并且不具有返回值类型,甚至连void也不写。
构造器的特点
- 构造器的名称必须与类名完全相同,包括大小写。
- 构造器没有返回值,也不能定义返回值类型;即构造器不能被
return
语句返回任何值。 - 一个类可以有一个或多个构造器。
- 构造器可以重载,即类可以包含多个构造器,它们的参数列表必须不同。
- 如果类中没有显式地定义任何构造器,Java编译器将会提供一个默认的无参构造器。一旦显式定义了任何构造器,无论是否有参数,编译器都不会提供默认的无参构造器。
- 构造器总是伴随着
new
关键字动态地在堆内存中创建对象。
构造器的用途
- 初始化对象:构造器的主要作用是在创建对象时初始化对象,为对象成员变量赋初始值。
- 创建实例时执行代码:构造器也是一个特殊的方法,可以包含任何合法的Java代码。一般情况下,构造器会执行所有出现在其主体中的语句。
以下是一个简单的Java类,它展示了如何使用构造器:
public class Student {
private String name;
private int age;
// 无参构造器
public Student() {
System.out.println("无参构造器被调用");
}
// 有参构造器
public Student(String name, int age) {
this.name = name;
this.age = age;
System.out.println("有参构造器被调用");
}
// 方法:获取学生信息
public String getInfo() {
return "姓名:" + name + ",年龄:" + age;
}
// ...其他getter和setter方法
}
public class Test {
public static void main(String[] args) {
// 使用无参构造器创建Student对象
Student s1 = new Student();
// 使用有参构造器创建Student对象
Student s2 = new Student("张三", 23);
// 打印信息
System.out.println(s2.getInfo());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
在上述代码中,Student
类有两个构造器:一个无参构造器和一个有参构造器。无参构造器在没有提供初始值时使用,有参构造器则允许在创建对象时立即给对象的属性赋值。通过new
关键字调用这些构造器时,可以根据需要选择适合的构造器。
# 7. Java Bean
Java Bean 是Java语言编写的可重用组件的标准形式。为了符合Java Bean的标准,一个类必须遵循以下几个简单的规则:
公共无参构造器:Java Bean 必须提供一个默认的无参构造器,且该构造器的访问权限为public。这样做是为了使Java Bean能够在实例化过程中,通过反射机制被轻松地创建。
私有属性:Java Bean 的属性通常使用private访问修饰符进行封装,以隐藏其内部实现细节,提高类的封装性。这样做的目的是为了通过公共方法(getter和setter方法)控制对这些属性的访问,而不是直接暴露给外部。
getter和setter方法:为了使外部代码能够访问Java Bean的私有属性,必须为每个属性提供公共的getter方法来获取属性值,以及setter方法来设置属性值。这些方法的命名通常遵循Java的命名规范,即对于一个属性name,其getter方法命名为getName(),setter方法命名为setName()。
序列化接口:为了使Java Bean能够轻松地保存其状态到文件中或在网络上进行传输,它可以实现java.io.Serializable接口。实现该接口的类可以被Java的序列化机制所序列化和反序列化,这是实现Java Bean持久性的关键。
Java Bean的这些特性使得它们非常适合用作在不同环境(如JSP/Servlet、EJB、远程方法调用等)中传递数据的载体。通过遵循这些简单的规则,Java Bean能够提供一种标准的方法来封装数据,使得数据操作更加安全、便捷。
import java.io.Serializable;
public class PersonBean implements Serializable {
private String name;
private int age;
// 公共无参构造器
public PersonBean() {}
// getter 和 setter 方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
以上就是Java Bean的一个简单示例,遵循了Java Bean的所有规范,使其成为一个合法的、可重用的组件。
# 8. this 关键字
在Java中,this
关键字是一个引用变量,它指向当前对象。这个关键字主要用于区分类成员和局部变量当它们的名字相同,以及在类的构造器之间进行相互调用。下面是this
关键字的几个主要用法和特点的详细总结:
# 8.1 区分成员变量和局部变量
当方法的形参(局部变量)与类的成员变量同名时,可以通过this
关键字来区分它们。this.变量名
指的是类的成员变量,而单独的变量名指的是方法的形参。
public class Example {
private int value;
public void setValue(int value) {
this.value = value; //这里的this.value指的是成员变量value,而单独的value指的是方法的形参
}
}
2
3
4
5
6
7
# 8.2 在构造器中调用其他构造器
this
关键字还可以用来在一个构造器中调用类的另一个构造器,以避免代码的重复。this()
调用无参构造器,而this(参数列表)
调用具有相应参数列表的构造器。这种调用必须位于构造器的第一行。
public class Student {
private String name;
private int age;
public Student() {
//无参构造器
}
public Student(String name) {
this.name = name;
}
public Student(String name, int age) {
this(name); //调用Student(String name)构造器
this.age = age;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 8.3 this关键字的限制
this()
和this(参数列表)
调用其他构造器的语句只能出现在构造器中,且必须是构造器的第一行代码。这确保了构造器在执行任何其他操作之前先初始化对象。- 不能在构造器中同时使用两种形式的
this
调用,因为每个构造器只能有一个这样的调用,并且它必须是第一条语句。 this
引用不能用在静态方法中,因为静态方法是属于类的,而不是属于任何特定对象的。
# 9. 继承性
在Java中,继承是面向对象三大主要特征之一(继承、封装、多态),它允许创建基于现有类的新类。这种机制有助于代码复用,并且可以建立一个类之间的层次体系。下面是关于Java继承性的详细总结:
基本概念
- is-a关系:继承表示一种"is-a"的关系,例如,"Student是一个Person",这表明Student类继承自Person类。
- 关键字:在Java中,使用
extends
关键字来建立类之间的继承关系。格式为public class 子类 extends 父类 {}
。
继承的特点
- 访问权限:子类不能直接访问父类中声明为
private
的成员变量和方法。若要访问这些私有成员,父类需要提供public
或protected
修饰的getter
和setter
方法。 - 单一父类:Java支持单继承,即一个子类只能有一个直接父类。这样做可以避免多重继承中可能出现的复杂性和歧义。
- 多级继承:虽然Java只支持单继承,但是可以通过多级继承形成一个继承体系。例如,类C可以继承类B,而类B又继承自类A。
- 构造器调用顺序:在创建子类的对象时,会先调用父类的构造器,然后再调用子类的构造器。这确保了父类的成员被正确初始化。如果子类的构造器没有通过
super()
显式调用父类的构造器,将自动调用父类的无参构造器。 - 静态成员和方法:静态成员和静态方法可以被继承,但是不能被子类重写(覆盖)。如果子类定义了一个与父类相同名称的静态方法,这将是隐藏父类的静态方法,而不是重写。
- 方法重写(Override):子类可以重写继承自父类的方法,以实现不同的功能。重写的方法必须有相同的方法名、参数列表和返回类型。
继承的好处
- 代码复用:继承允许子类复用父类的字段和方法,减少了代码的重复编写。
- 扩展性:通过继承,可以在现有类的基础上扩展新的功能,易于应对程序的变化。
- 为多态性提供基础:继承是实现多态性(同一个行为具有多个不同表现形式)的基础。
注意事项
- 在使用继承时,应当遵循“is-a”的原则,确保子类确实是父类的一种特殊形态。
- 应当尽量减少使用继承带来的耦合,合理设计类的层次结构,以免过度使用继承导致的结构复杂和理解困难。
- 考虑到封装性,不应该让子类直接访问父类中的内部数据。通过提供访问器(getter)和修改器(setter)来访问和修改父类私有成员。
# 10. 重写(overwrite、override)
在Java中,方法的重写(Override)是面向对象编程中的一个重要特性,它允许子类提供特定于自身的实现,替换从父类继承的方法。下面是关于方法重写的详细总结,包括定义、规则和示例:
方法重写:当子类需要一个与继承自父类同名的方法,但要在子类中执行不同的功能时,子类可以重写父类中的方法。
方法重写的规则
- 方法名和参数列表:重写的方法必须与被重写的方法
具有相同的方法名称和参数列表
。 - 访问修饰符:子类重写的方法的访问权限
不能小于
父类被重写方法的访问权限。 - 返回值类型:
- 父类方法的返回值类型是
基本数据类型或void
,则子类重写的方法的返回值类型必须与父类方法的返回值类型相同
。 - 父类方法的返回值类型是
引用数据类型
,则子类重写的方法的返回值类型可以是父类方法的返回值类型或其子类型
。
- 父类方法的返回值类型是
- 抛出的异常:子类重写的方法所抛出的异常类型
不能大于
父类被重写方法所声明的异常类型。 - 静态方法:静态方法不能被重写为非静态方法(反之亦然)。如果子类定义了一个与父类中相同签名的静态方法,则该方法不是重写,而是隐藏父类的方法。
@Override 注解
- 使用
@Override
注解可以帮助检查重写方法的正确性。如果方法确实重写了父类中的方法,编译器就会编译;否则,编译器会报错。
示例
class Animal {
public void eat() {
System.out.println("Animal is eating");
}
}
class Dog extends Animal {
@Override
public void eat() {
System.out.println("Dog is eating");
}
public void eat(String food) {
System.out.println("Dog is eating " + food);
}
}
public class TestOverride {
public static void main(String[] args) {
Animal myDog = new Dog();
myDog.eat(); // 输出: Dog is eating
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
在上面的示例中,Dog
类重写了从Animal
类继承的eat()
方法。当通过Animal
类型的引用调用eat()
方法时,实际执行的是Dog
类中重写后的方法。这个例子也展示了多态的一种应用。
注意事项
- 重写不同于重载(Overload),重载是指在同一个类中存在多个方法名相同但参数列表不同的方法。
- 在设计类的继承体系时,合理使用方法重写可以提高代码的可读性和可维护性。
# 11. super 关键字
super
关键字在Java中扮演着重要的角色,主要用于访问父类的属性、方法和构造器。以下是对super
使用的详细总结:
访问父类的属性和方法
- 属性就近原则:当子类和父类存在同名属性时,子类中的方法默认访问子类自己的属性。如果需要访问父类中的同名属性,则需要使用
super.属性名
。 - 方法的访问规则:
- 如果方法前没有使用
super.
或this.
,Java会先从当前类(子类)开始查找该方法,如果没有找到,再按继承链向上查找直到找到为止。 - 使用
this.方法名()
强调的是从当前类开始查找方法。 - 使用
super.方法名()
直接跳过当前类,从父类开始查找该方法。
- 如果方法前没有使用
- 总结:
super
关键字使得子类能够明确地访问父类的属性和方法,尤其是在有同名成员的情况下。
调用父类的构造器
- 使用
super(形参列表)
:在子类的构造器中,使用super(形参列表)
显式地调用父类的构造器,且这个调用必须位于子类构造器的第一行。 this
和super
互斥:在构造器中,this(形参列表)
调用和super(形参列表)
调用不能同时存在,因为它们都必须位于构造器的第一行。- 默认调用父类无参构造器:如果子类构造器中没有通过
this(形参列表)
或super(形参列表)
显式地调用其他构造器,那么默认会调用父类的无参构造器super()
。
构造器调用链
- 当通过子类构造器创建对象时,这个构造器会直接或间接地调用其父类的构造器,进而沿着继承链一直调用到
Object
类的构造器。这个过程确保了所有父类都被正确初始化。
示例代码
class Person {
String name = "Person";
public void show() {
System.out.println("我是Person的show方法");
}
}
class Student extends Person {
String name = "Student";
public void show() {
System.out.println("我是Student的show方法");
System.out.println("name = " + name); // 访问自己的属性
System.out.println("super.name = " + super.name); // 访问父类的属性
}
public Student() {
super(); // 显式调用父类的无参构造器,可以省略
}
public void callParentShow() {
super.show(); // 调用父类的show方法
}
}
public class TestSuper {
public static void main(String[] args) {
Student s = new Student();
s.show();
s.callParentShow();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
在这个示例中,Student
类通过使用super
关键字明确地访问了父类Person
的属性和方法。同时,Student
的构造器中通过super()
显式地调用了父类Person
的无参构造器,虽然这个调用可以省略。
# 12. 多态性
多态的使用前提:① 类的继承关系 ② 方法的重写
在Java中,多态性主要体现为父类的引用可以指向子类的对象, 比如:Person p = new Student();
。这意味着,通过父类的引用,我们可以在运行时调用到实际子类对象的重写方法,从而实现不同表现形式。
常用多态的地方:
方法的形参:在定义方法时,可以将父类类型作为参数类型。这样,该方法就可以接收任何该父类及其子类的对象作为实参,提高了方法的通用性。
方法的返回值:方法可以返回一个父类类型的引用,而实际返回的是子类对象的引用,使得方法的返回值更加灵活。
父类引用作为方法的形参,是多态使用最多的场合。即使增加了新的子类,原方法也无需改变,提高了扩展性,符合开闭原则(对扩展开放,对修改关闭)。
class Animal {
void eat() {
System.out.println("Animal eats");
}
}
class Dog extends Animal {
@Override
void eat() {
System.out.println("Dog eats");
}
void bark() {
System.out.println("Dog barks");
}
}
public class TestPolymorphism {
public static void main(String[] args) {
Animal animal = new Dog(); // 父类引用指向子类对象
animal.eat(); // 输出: Dog eats,体现多态性
// 若要调用Dog特有的方法,需要向下转型
if (animal instanceof Dog) {
Dog dog = (Dog) animal;
dog.bark(); // 输出: Dog barks
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
注意事项
- 使用父类引用时,我们不能直接访问子类中特有的属性和方法。这意味着,如果子类中有一些新增的方法或属性,这些成员无法通过父类的引用直接调用。
- 在Java中,除了那些被子类重写的方法调用发生动态绑定外,其它所有类型的成员访问都是静态绑定的。
- 所以在多态的情况下,如果父类和子类有同名的属性,通过父类的引用访问的总是父类中定义的属性,而不是子类中的属性。这是因为在Java中,属性的访问是静态绑定的,即它在编译时就已经确定了。
如果要访问子类中特有的属性和方法,只有先向下转型,才能访问。
class Parent {
int num = 10;
}
class Child extends Parent {
int num = 20;
}
public class Test {
public static void main(String[] args) {
Parent obj = new Child();
System.out.println(obj.num); // 成员变量不参与多态,所以输出结果10
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
总结
在Java中,多态主要体现在方法的重写上,允许子类提供特定实现的方法来覆盖父类的方法。然而,成员变量和静态方法不遵循这样的动态绑定原则,它们的访问决定于引用变量的类型而不是实例的类型。这就意味着,即使在多态的情况下,成员变量和静态方法的行为就像它们不参与多态一样,始终遵循静态绑定的规则。
# 12.1 虚方法调用(Virtual Method Invocation)
Java 中的虚方法机制是多态(Polymorphism)的一个关键实现技术。在Java中,大部分方法默认情况下都是虚方法(除了私有方法(private)、静态方法(static)、最终方法(final)以及构造方法),这意味着方法的调用不是在编译时期决定的,而是在运行时期根据对象的实际类型来动态决定的。
在编译阶段,编译器只能知道调用方法名和参数信息,但是具体调用哪个类中的实现(即方法在内存中的地址)是在运行时才能确定的。如果子类重写了父类的方法,当通过父类引用调用该方法时,JVM会根据引用所指向的实际对象类型来调用相应子类中的重写方法,这个过程称为动态绑定或晚期绑定。
这种机制允许Java实现运行时多态。例如,如果有一个父类Animal
和它的子类Dog
,Dog
重写了Animal
中的eat()
方法。当通过Animal
类型的引用调用eat()
方法时,若引用指向的是一个Dog
对象,则实际调用的是Dog
中重写后的eat()
方法,而不是Animal
中原有的方法。
class Animal {
public void eat() {
System.out.println("Animal eats");
}
}
class Dog extends Animal {
@Override
public void eat() {
System.out.println("Dog eats");
}
}
public class Test {
public static void main(String[] args) {
Animal a = new Dog();
a.eat(); // 输出 "Dog eats"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在这个例子中,虽然变量a
的编译时类型是Animal
,但其运行时类型是Dog
,因此调用的是Dog
类中重写的eat()
方法,而不是Animal
中的原始版本。这就是虚方法调用的实质。
# 13. 向下转型
在Java中,向下转型(downcasting)是对象引用转换的一种,它将父类的引用转换为子类的引用。这个过程允许我们访问子类中特有的成员(属性或方法)。向下转型通常伴随着instanceof
关键字的使用,以确保转型的安全性。
向下转型主要用于多态情况下,当你通过父类引用访问子类对象,但需要调用子类特有的方法或属性时,你必须进行向下转型。
# 13.1 instanceof
在进行向下转型之前,通常需要使用instanceof
运算符来检查被引用的对象是否是特定类型(或该类型的子类型)。instanceof
运算符的使用减少了运行时错误的风险,如ClassCastException
。
- 语法:
对象引用 instanceof 类型
- 如果左边的对象是右边类型或子类型的实例,返回
true
,否则返回false
。
向下转型需要显式的类型转换,语法如下:
子类类型 变量名 = (子类类型) 父类引用;
示例:假设我们有一个Animal
类和它的子类Dog
:
class Animal {
void eat() {
System.out.println("Animal eats");
}
}
class Dog extends Animal {
void bark() {
System.out.println("Dog barks");
}
}
2
3
4
5
6
7
8
9
10
11
在多态的情况下,我们可能有如下的代码:
Animal animal = new Dog();
animal.eat(); // 正常调用,因为eat()在Animal中定义
2
但如果我们想调用Dog
特有的bark()
方法,直接调用会导致编译错误,因为animal
的类型是Animal
,它不包含bark()
方法。这时我们需要向下转型:
if(animal instanceof Dog) {
Dog dog = (Dog) animal;
dog.bark(); // 现在可以调用了
}
2
3
4
这里,我们首先检查animal
引用的对象是否真的是Dog
的实例,如果是,我们才进行向下转型并调用bark()
方法。这种方式保证了类型转换的安全性。
# 14. Object 类
如果一个类没有显式继承父类,那么默认则继承 Object 类。
Object 类没有属性,只有方法。
# 14.1 clone()
克隆出一个新对象。
步骤:
- 被克隆的类实现Cloneable接口
- 通过
对象.clone()
产生一个新对象。
会抛出异常,需要
try...catch
接收。
@Test
public void test(){
Animal a1 = new Animal();
try {
Animal a2 = (Animal)a1.clone();
System.out.println(a1==a2); //false,
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
//因为 clone() 的返回值为 Object,这里需要强转,a2 和 a1 是 2 个不同的对象。
class Animal implements Cloneable{ //需要实现Cloneable接口
int age =10;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 14.2 finalize()
当 GC(Garbage Collection,垃圾回收器)要回收对象时,可以调用此方法。在子类中重写此方法,可在释放对象前进行某些操作。
在 JDK 9 中此方法已经被标记为过时的。
# 14.3 getClass()
获取对象的运行时类型。
Base sub = new Sub();
System.out.println(sub.getClass());//输出为:class com.lc.Sub
2
# 14.4 equals()[重要]
Object
的 equals()
相当于 “==
”。
“==
”对于基本数据类型(会自动类型提升),比较值是否相等;而对于引用数据类型,比较地址值是否相等。
对于引用数据类型,使用“
==
”时,对象名对应的类型要么相同,要么有继承关系,才能过编译(和instanceof
一个要求)。
格式:obj1.equals(obj2)
所有类都继承了 Object,也就获得了 equals()
方法,并且可以重写。
File、String、Date
及包装类重写了 equals()
方法,比较的是引用类型及内容
,而不是地址值。
类有属性时,按
Alt+ Insert
可以快速重写。没有属性时,重写没有实际效果。
# 14.5 toString()[重要]
打印引用数据类型变量,默认会调用 toString()
。
在自定义类,没有重写 toString()
的情况下,打印的是“对象的运行时类型@对象的hashCode值的十六进制形式
"
System.out.println(base); //com.lc.Base@1b6d3586
可以根据用户需求,重写 toString()
方法,例如 String
类重写了 toString()
方法,可以返回字符串的值。
@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + ", age=" + age +'}';
}
2
3
4
# 15. static
在Java中,static
关键字用于声明类的成员变量和方法为静态的,它们属于类本身,而不是类的任何特定实例。
静态变量(类变量)
- 定义和特性:静态变量也称为类变量,用
static
修饰符声明。它们在类加载时初始化,位于内存中的静态区域(在JDK 6中是方法区,在JDK 7及以后版本中被移动到了堆空间中),并且在程序运行期间只有一份副本。 - 共享性:静态变量被类的所有实例共享。这意味着,所有实例都访问和修改的是同一变量。
- 访问方式:可以通过类名直接访问静态变量,也可以通过类的实例访问,但推荐使用类名访问,以强调其静态特性。
静态方法(类方法)
- 定义和特性:静态方法是使用
static
关键字声明的方法。它们随类加载而加载,可以通过类名或类的实例调用。 - 限制:静态方法
只能直接访问类的静态成员(变量和方法)
,不能直接访问类的实例成员
。静态方法中不能使用this
和super
关键字,因为这两个关键字与具体的实例相关,而静态方法在没有任何实例的情况下就可以被调用。
class Base {
static int a = 1;
}
public class ChildTest {
public static void main(String[] args) {
Base base = null;
System.out.println(base.a); // 输出1
}
}
2
3
4
5
6
7
8
9
10
上面的例子展示了即使
base
引用为null
,通过它访问静态变量a
仍然是有效的,因为静态变量a
属于类Base
本身,而与base
引用的具体对象无关。类的实例也可以作为静态成员,如下所示:
class A { static A a = new A(); }
1
2
3这个例子中,
A
类有一个静态变量a
,它被初始化为A
类的一个新实例。这个静态实例变量a
随类A
的加载而加载,并且在整个程序运行期间只有这一个实例。
小结
静态成员(变量和方法)为类级别的成员,与类的任何具体实例无关,它们在类加载时初始化,整个程序运行期间只有一份副本。静态变量可以用于存储类级别的信息,而静态方法通常用于执行不依赖于对象状态的操作。
# 16. native 关键字
当一个方法被 native 修饰时,说明这个方法是用非 Java 的编程语言写的。
# 17. 类成员-代码块(初始化块)
类初始化和实例化执行顺序
父类静态代码块优先于子类静态代码块执行:这是因为Java类加载机制的一部分。首先加载父类,然后才是子类,所以父类的静态代码块比子类的先执行。
父类实例代码块和父类构造方法紧接着执行:当创建类的实例时,实例初始化包括父类的非静态初始化块(实例代码块)和父类构造方法的执行,按它们在父类中的出现顺序执行。
子类的实例代码块和子类构造方法紧接着再执行:完成父类的初始化后,子类的非静态初始化块和构造方法会按照它们在子类中的出现顺序执行。
第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行:静态代码块只在类首次加载到JVM时执行一次,之后的实例化不会再执行静态代码块。
静态变量和静态代码块按照在类中出现的顺序执行
如果静态变量出现在静态代码块之前,那么静态变量先初始化,静态代码块后执行;反之,则静态代码块先执行,静态变量后初始化。
class Parent {
static {
System.out.println("1. 父类的静态代码块");
}
{
System.out.println("3. 父类的实例代码块");
}
public Parent() {
System.out.println("4. 父类的构造方法");
}
}
class Child extends Parent {
static {
System.out.println("2. 子类的静态代码块");
}
{
System.out.println("5. 子类的实例代码块");
}
public Child() {
System.out.println("6. 子类的构造方法");
}
public static void main(String[] args) {
System.out.println("=== 第一次实例化子类 ===");
new Child();
System.out.println("\n=== 第二次实例化子类 ===");
new Child();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
输出结果解释
- 在第一次实例化
Child
类的对象时,输出顺序表明了静态代码块(先父类后子类)只执行一次,实例代码块和构造方法(先父类后子类)的执行顺序。 - 在第二次实例化
Child
类的对象时,只有实例代码块和构造方法会再次执行,静态代码块不会执行,因为它们只在类第一次加载时执行一次。
# 18. final
# 18.1 final修饰类
当
final
修饰一个类时,这个类不能被继承。尝试继承一个被final
修饰的类将导致编译错误。这通常用于增强安全性,或者当一个类已经被设计得足够完善且不需要任何改变时。示例:
final class MyFinalClass { // 类的内容 }
1
2
3
# 18.2 final修饰方法
final
修饰的方法不能被子类重写。这通常用于锁定方法的实现,防止任何修改其行为的尝试。示例:
class SuperClass { final void finalMethod() { // 方法实现 } }
1
2
3
4
5
# 18.3 final修饰变量
final
修饰的变量称为最终变量,意味着一旦给它赋值后,它的值就不能被改变(初始化之后的任何修改尝试都将导致编译错误)。final
变量可以是成员变量或局部变量。
# 18.3.1 成员变量
必须在构造对象完成之前被初始化(显式初始化、代码块中赋值、构造器中赋值)。
示例:
class WithFinalField { final int finalField = 10; // 显式初始化 final int anotherFinalField; { anotherFinalField = 20; // 代码块中赋值 } final int yetAnotherFinalField; WithFinalField() { yetAnotherFinalField = 30; // 构造器中赋值 } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 18.3.2 局部变量
方法内的局部
final
变量或方法的final
形参,在声明时可以不立即初始化,但在使用之前必须被初始化,并且一旦被赋值后,其值不能被改变。示例:
void myMethod(final int param) { final int localVar; localVar = 100; // 初始化局部final变量 // localVar = 200; // 错误:无法再次赋值 }
1
2
3
4
5
# 18.4 final与static一起使用
final
和static
一起修饰变量时,这个变量成为全局常量。通常全大写字母表示,单词之间用下划线连接。示例:
class Constants { public static final String MY_CONSTANT = "SomeValue"; }
1
2
3
# 18.5 final修饰引用变量
当
final
修饰引用类型变量时,这意味着引用本身不可更改,即不能将其重新指向另一个对象。但是,这个引用指向的对象的内容是可以修改的。示例:
final MyClass myClass = new MyClass(); myClass.aField = 10; // 可以修改对象的属性 // myClass = new MyClass(); // 错误:无法改变myClass的引用
1
2
3
final
关键字提供了一种机制,通过它可以指定某些内容是不可变的。正确使用final
可以使Java程序更安全、清晰和健壮。
# 19. abstract
抽象类是Java中实现抽象机制的一个核心概念,它为我们定义了一种特殊类型的类,以下是对抽象类的详细总结:
定义与目的:抽象类是一种不能被实例化的类,其目的在于为其他类提供一个可以继承的基类。抽象类通常包含一个或多个抽象方法,这些方法没有具体的实现,即没有方法体。它们是用来描述具有共同特性的对象的基本结构和行为。
抽象方法:在抽象类中声明的方法,如果没有实现(即没有方法体),则必须使用
abstract
关键字进行修饰,声明为抽象方法。抽象方法的实现由继承抽象类的子类负责。
抽象类的特点
- 不能被实例化:不能使用
new
关键字直接创建抽象类的实例。这是因为抽象类通常不包含足够的信息来描述一个具体的对象。 - 可以包含构造器:虽然抽象类不能直接实例化,它可以包含构造器。这些构造器不是用于创建抽象类的对象,而是在子类的对象创建过程中被调用,用于初始化子类对象的共通属性。
- 可以包含实现的方法:抽象类不仅可以包含抽象方法,还可以包含具有完整实现的方法。这些方法可以被子类直接使用或者被子类重写。
- 继承:抽象类可以被其他的抽象类或具体类继承。继承抽象类的子类必须实现父类中的所有抽象方法,除非子类也被声明为抽象类。
抽象关键字
abstract
不能与哪些关键字共存
final
:final
关键字表示最终的,不能被改变的。一个类被声明为final
时,它不能被继承,这与抽象类的设计初衷相违背。static
:静态方法不能被覆盖,而抽象方法需要被子类实现和覆盖,因此abstract
不能与static
共用。private
:私有方法无法在类的外部被访问,而抽象方法需要被子类实现,因此abstract
不能与private
共用。
抽象类的作用
- 定义标准:抽象类可以用来定义子类必须实现的行为接口,确保所有子类都具有一致的基本行为。
- 代码复用:抽象类允许将共通的代码和接口定义在一个地方,减少代码重复。
- 多态性:抽象类允许使用多态,即父类引用指向子类对象,这在运行时可以实现不同形式的方法调用。
public abstract class Person {
public void eat() {
System.out.println("团团今天没吃饭!");
}
public abstract void say();
public abstract void sleep();
}
class Student extends Person {
@Override
public void say() {
System.out.println("我喜欢演讲!");
}
@Override
public void sleep() {
System.out.println("我喜欢睡觉!");
}
}
class Demo {
public static void main(String[] args) {
// Person p = new Person(); // 错误:不能实例化抽象类
Student s = new Student(); // 创建子类对象
s.sleep();
s.say();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 20. 接口
# 20.1 接口的定义
接口是一种规范。想要有某种功能,实现对应的接口就行。类和接口是“has-a”的关系,接口和类不是继承关系,而是实现关系。一个类可以实现多个接口。
定义:使用interface
关键字定义接口。
属性:接口中定义的属性默认为public static final
,即公共静态常量。属性的值在定义时必须初始化,且不可改变。
方法:
- 抽象方法:在JDK 7及之前,接口中只能包含抽象方法,这些方法默认为
public abstract
,这两个修饰符可以省略不写。 - 静态方法:从JDK 8开始,接口中允许定义静态方法,必须提供方法体,可以通过接口直接调用,静态方法不能被实现类继承或调用。
- 默认方法:JDK 8引入,默认方法使用
default
关键字标识,允许有实现体。实现接口的类可以不用实现默认方法,也可以选择重写它。如果需要在重写的默认方法中调用接口的默认方法,可以使用接口名.super.方法名();
的方式。 - 私有方法:JDK 9引入,接口中可以定义私有方法,主要用于默认方法或其他私有方法内部调用,以避免代码重复。
构造器和代码块:接口不能包含构造器和初始化代码块。
继承:接口可以继承一个或多个其他接口,使用extends
关键字。
接口的多态性
- 接口可以实现多态,通过接口类型的引用来指向实现了该接口的类的对象。
- 使用接口类型的引用,只能调用接口中定义的方法,不能调用实现类中独有的方法。
接口类型的数组
- 可以创建接口类型的数组,数组中的元素必须是实现了接口且重写了接口中所有抽象方法的类的实例。
interface Eatable {
void eat();
}
class Chinese implements Eatable {
@Override
public void eat() {
System.out.println("用筷子吃饭");
}
}
class American implements Eatable {
@Override
public void eat() {
System.out.println("用刀叉吃饭");
}
}
public class TestInterface {
public static void main(String[] args) {
Eatable[] eatables = new Eatable[2];
eatables[0] = new Chinese();
eatables[1] = new American();
for (Eatable e : eatables) {
e.eat(); // 多态调用
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
上述演示了如何定义和实现接口,以及如何通过接口实现多态。通过接口Eatable
定义了一个eat
方法,然后由Chinese
和American
两个类实现了这个接口,并提供了eat
方法的具体实现。在TestInterface
主类中,通过接口类型的数组存储了不同的实现类对象,并在遍历数组时,通过接口调用了各自的eat
方法。
# 20.2 冲突问题
在Java中,当类与接口之间发生冲突,特别是在方法和属性命名相同的情况下,Java提供了一套规则来解决这些冲突。以下是关于方法冲突和属性冲突解决方式的详细总结:
方法冲突
当一个类同时继承一个父类和实现一个或多个接口,且父类的方法和接口的默认方法发生了名称上的冲突时,遵循如下原则:
父类优先原则:如果子类没有重写这个方法,那么子类继承或者说默认执行的是父类中的方法。Java给予类继承的方法优先权,接口中具有相同名称和参数的默认方法会被忽略。
重写解决冲突:
- 不重写:默认保留并使用父类的方法。
- 调用父类方法:在子类重写的方法里,可以通过
super.方法名()
调用父类中被覆盖的方法。 - 调用接口的默认方法:如果想要在子类中使用接口的默认方法,可以通过
接口名.super.方法名()
的方式调用。 - 完全重写:子类可以完全重写该方法,自定义方法体,不调用父类或接口中的任何实现。
属性冲突
当父类的属性和接口中的属性同名时,处理属性冲突的方式略有不同,因为属性并不像方法那样可以被重写。解决方法如下:
- 访问父类属性:通过
super.属性名
在子类中访问父类的属性。 - 访问接口属性:因为接口中的属性默认是
public static final
的,所以可以通过接口名.属性名
直接访问接口中的属性。
假设有一个父类Parent
和一个接口MyInterface
,它们都有一个名为method
的方法和一个名为value
的属性:
class Parent {
public void method() {
System.out.println("Parent method");
}
public int value = 10;
}
interface MyInterface {
default void method() {
System.out.println("MyInterface default method");
}
int value = 20; // 注意:接口中的属性默认是 public static final 的
}
class Child extends Parent implements MyInterface {
// 方法冲突解决
@Override
public void method() {
super.method(); // 调用父类的方法
MyInterface.super.method(); // 调用接口的默认方法
System.out.println("Child method");
}
public void showValue() {
System.out.println("Parent value: " + super.value); // 访问父类属性
System.out.println("MyInterface value: " + MyInterface.value); // 访问接口属性
}
}
public class Test {
public static void main(String[] args) {
Child child = new Child();
child.method(); // 展示方法冲突的解决
child.showValue(); // 展示属性冲突的解决
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
以上代码演示了如何在子类Child
中处理和解决方法和属性冲突,包括如何同时调用父类方法和接口中的默认方法,以及如何区分同名的父类属性和接口属性。
# 21. 内部类(InnerClass)
将一个类 A 定义在另一个类 B 里面,里面的那个类 A 就称为内部类(InnerClass)
,类 B 则称为外部类(OuterClass)
。
当一个事物 A 的内部的某部分是一个完整结构 B ,而结构 B 又只为事物 A 提供服务,不在其他地方单独使用,那么完整结构 B 最好使用内部类。
成员内部类也是类的一种:
- 可以在内部定义属性、方法、构造器等结构。
- 可以继承,可以实现接口。
- 可以声明为
abstract
类 ,因此可以被其它的内部类继承。 - 可以声明为
final
的,此时不能被继承。
成员内部类看成外部类的成员:
- 内部类可以调用外部类的属性或方法(静态内部类不能使用外部类的非静态成员)。
- 成员内部类四种权限修饰符都能用,而外部类只能用
public
与缺省修饰符。 - 可以用
static
修饰。
# 静态内部类: 定义在类内部的静态类,就是静态内部类
静态内部类可以访问外部类所有的静态变量,而不可访问外部类的非静态变量;静态内部类的创建方式,new 外部类.静态内部类(),如下:
Outer.StaticInner inner = new Outer.StaticInner();
inner.visit();
2
# 成员内部类: 定义在类内部,成员位置上的非静态类,就是成员内部类
成员内部类可以访问外部类所有的变量和方法,包括静态和非静态,私有和公有。成员内部类依赖于外部类的实例,它的创建方式外部类实例.new 内部类(),如下:
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.visit();
2
3
# 局部内部类: 定义在方法中的内部类,就是局部内部类
定义在实例方法中的局部类可以访问外部类的所有变量和方法,定义在静态方法中的局部类只能访问外部类的静态变量和方法。局部内部类的创建方式,在对应方法内,new 内部类(),如下:
public static void testStaticFunctionClass(){
class Inner {
}
Inner inner = new Inner();
}
2
3
4
5
# 匿名内部类: 匿名内部类就是没有名字的内部类,日常开发中使用的比较多
匿名内部类创建方式:
new 类/接口{
//匿名内部类实现部分
}
2
3
除了没有名字,匿名内部类还有以下特点
:
- 匿名内部类必须继承一个抽象类或者实现一个接口。
- 匿名内部类不能定义任何静态成员和静态方法。
- 当所在的方法的形参需要被匿名内部类使用时,必须声明为 final。
- 匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。
# 内部类的优点
我们为什么要使用内部类呢?因为它有以下优点:
- 一个内部类对象可以访问创建它的外部类对象的内容,包括私有数据!
- 内部类不为同一包的其他类所见,具有很好的封装性;
- 内部类有效实现了“多重继承”,优化 java 单继承的缺陷。
- 匿名内部类可以很方便的定义回调。
局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final?(变量捕获)
- 局部内部类和匿名内部类访问局部变量的时候,为什么变量必须要加上final呢?它内部原理是什么呢?先看这段代码:
public class Outer {
void outMethod(){
final int a =10;
class Inner {
void innerMethod(){
System.out.println(a);
}
}
}
}
2
3
4
5
6
7
8
9
10
11
- 生命周期问题:局部变量的生命周期只在方法内,而局部内部类或匿名内部类可能在方法外仍然存在。设为final确保了即使局部变量销毁,其值也能被内部类安全访问。
- 值一致性:将局部变量设置为final保证了在局部内部类或匿名内部类的整个生命周期中,其看到的值都是一致的。
- 线程安全:final标记防止了可能的并发问题,增强了线程安全性。
- Java 8开始了简化:即使没有明确标记为final,局部变量仍然上是不可变的,如果修改在内部类中修改局部变量就会报错 (编译器会隐式处理这种情况)。
# 内部类相关,看程序说出运行结果
public class Outer {
private int age = 12;
class Inner {
private int age = 13;
public void print() {
int age = 14;
System.out.println("局部变量:" + age);
System.out.println("内部类变量:" + this.age);
System.out.println("外部类变量:" + Outer.this.age);
}
}
public static void main(String[] args) {
Outer.Inner in = new Outer().new Inner();
in.print();
}
}
局部变量:14
内部类变量:13
外部类变量:12
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 22. 总结
以上便是本文的全部内容,本人才疏学浅,文章有什么错误的地方,欢迎大佬们批评指正!我是scholar,一个在互联网行业的小白,立志成为更好的自己。
如果你想了解更多关于scholar (opens new window),可以关注公众号-书生带你学编程,后面文章会首先同步至公众号。