当谈到Java中的类和对象时,类是一种抽象的模板,用于描述具有相同属性和行为的对象的集合。而对象是类的一个具体实例,它具有类定义的属性和行为。
1. 定义类
在Java中,类通过class关键字来定义,类定义包含了类名、类的属性和方法。
语法规则:
java
class ClassName {
// 类的属性
dataType attributeName;
// 类的方法
returnType methodName(parameterList) {
// 方法体
}
}
示例代码:
java
class Car {
// 类的属性
String color;
int speed;
// 类的方法
void drive() {
System.out.println("Driving at speed: " + speed);
}
}
2. 创建和使用类
要创建类的对象,使用new关键字并调用类的构造函数。一旦创建了对象,就可以使用点.运算符来访问对象的属性和调用对象的方法。
语法规则:
java
ClassName objectName = new ClassName();
示例代码:
java
public class Main {
public static void main(String[] args) {
// 创建Car类的对象
Car myCar = new Car();
// 设置对象的属性
myCar.color = "Red";
myCar.speed = 60;
// 调用对象的方法
myCar.drive();
}
}
实例变量属于成员变量,成员变量如果没有手动赋值,系统会赋默认值。
| 数据类型 | 默认值 |
|---|---|
| byte | 0 |
| short | 0 |
| int | 0 |
| long | 0L |
| float | 0.0f |
| double | 0.0d |
| char | '\u0000' |
| boolean | false |
| 引用类型 | null |
| 类 = 属性 + 方法。属性:描述的是状态。方法:描述的是行为。我们通常描述一个对象的行为动作时,不加static。没有添加static的方法,被叫做:实例方法。(对象方法)。实例变量要想访问,必须先new对象。通过引用来访问实例变量。实例变量是不能通过类名直接访问的。同样地,实例方法不能使用"类名."去调用。 |
java
public class Vip {
// 类 = 属性 + 方法
// 属性:描述的是状态。
// 方法:描述的是行为。
// 姓名
String name; // 实例变量(对象变量)
// 年龄
int age; // 实例变量(对象变量)
// 会员行为动作
// 购物行为
// 先记住:我们通常描述一个对象的行为动作时,不加static。
// 没有添加static的方法,被叫做:实例方法。(对象方法)
public void shopping(){
System.out.println("正在购物!");
}
}
java
public class VipTest01 {
public static void main(String[] args) {
// 创建一个Vip对象
Vip vip1 = new Vip();
// 给name和age属性赋值
vip1.name = "jack";
vip1.age = 20;
System.out.println("name = " + vip1.name);
System.out.println("age = " + vip1.age);
// 去购物
vip1.shopping();
// 再创建一个Vip对象
Vip vip2 = new Vip();
vip2.name = "lisi";
vip2.age = 15;
System.out.println("name = " + vip2.name);
System.out.println("age = " + vip2.age);
// 去购物
vip2.shopping();
// 为什么name和age不能使用"类名."访问。
// 实例变量要想访问,必须先new对象。通过引用来访问实例变量。
// 实例变量是不能通过类名直接访问的。
//System.out.println(Vip.name);
//System.out.println(Vip.age);
// 编译报错。实例方法不能使用"类名."去调用。
//Vip.shopping();
}
}
2.1方法参数传递的内存分析
定义User类
java
public class User {
int age;
}
基本数据类型:
java
public class ArgsTest01 {
public static void main(String[] args) {
int i = 10;
// 调用add方法的时候,将i传进去,实际上是怎么传的?将i变量中保存值10复制了一份,传给了add方法。
add(i);
System.out.println("main--->" + i); // 10
}
public static void add(int i){ // 方法的形参是局部变量。
i++;
System.out.println("add--->" + i); // 11
}
}

引用数据类型:
java
public class ArgsTest02 {
public static void main(String[] args) {
User u = new User();
u.age = 10;
// u是怎么传递过去的。实际上和i原理相同:都是将变量中保存的值传递过去。
// 只不过这里的u变量中保存的值比较特殊,是一个对象的内存地址。
add(u);
System.out.println("main-->" + u.age); // 11
}
public static void add(User u) { // u是一个引用。
u.age++;
System.out.println("add-->" + u.age); // 11
}
}

3.初识this关键字
this 本质上是一个引用。保存的是当前对象的内存地址。
java
public class Student {
String name;
public void study(){
//System.out.println(this.name + "正在努力的学习!");
// this. 是可以省略。默认访问的就是当前对象的name。
System.out.println(name + "正在努力的学习!!!!!!!");
}
}
java
public class StudentTest {
public static void main(String[] args) {
Student zhangsan = new Student();
zhangsan.name = "张三";
// 让张三去学习。
zhangsan.study();
Student lisi = new Student();
lisi.name = "李四";
// 让李四去学习。
lisi.study();
}
}
4.OOP三大特征之封装
4.1为什么使用封装?
先不使用封装机制,分析程序存在哪些问题?
java
public class User {
int age;
}
java
public class UserTest {
public static void main(String[] args) {
User user = new User();
System.out.println("年龄:" + user.age); // 0
user.age = 50;
System.out.println("年龄:" + user.age); // 50
// 目前User类没有进行封装,在外部程序中可以对User对象的age属性进行随意的访问。
// 这样非常不安全的。(因为现实世界中age不可能是负数。如果是真正的业务,-100不应该能够赋值给age变量。)
user.age = -100;
System.out.println("年龄:" + user.age); // -100
}
}
User类型对象的age属性非常不安全。在外部程序中可以对其随意的访问。
为了保证User类型对象的age属性的安全,我们需要使用封装机制。
4.2实现封装的步骤:
- 第一步:属性私有化。(什么是私有化?使用 private 进行修饰。) 属性私有化的作用是:禁止外部程序对该属性进行随意的访问。所有被private修饰的,都是私有的,私有的只能在本类中访问。
- 第二步:对外提供setter和getter方法。 为了保证外部的程序仍然可以访问age属性,因此要对外提供公开的访问入口。那么应该对外提供两个方法,一个负责读,一个负责修改。 读方法的格式:
public int getAge(){}。getter方法是绝对安全的。因为这个方法是读取属性的值,不会涉及修改操作。 改方法的格式:public void setAge(int age){}。setter方法当中就需要编写拦截过滤代码,来保证属性的安全。
java
public class User {
private int age;
public int getAge(){
//return this.age;
return age;
}
/*public void setAge(int num){
//this.age = num;
if(num < 0 || num > 100) {
System.out.println("对不起,您的年龄值不合法!");
return;
}
age = num;
}*/
// java有就近原则。
public void setAge(int age){
if(age < 0 || age > 100) {
System.out.println("对不起,您的年龄值不合法!");
return;
}
// this. 大部分情况下可以省略。
// this. 什么时候不能省略?用来区分局部变量和实例变量的时候。
this.age = age;
}
}
为了使setAge方法的参数更加符合实际意义。所以要用age,但是age又和成员变量冲突。需要用this关键字来进行区分。
4.3实例方法调用实例方法

调用方法是需要引用的,但是我们常常省略了this关键字的引用。
5.构造方法(Constructor)
- 构造方法有什么作用? 作用1:对象的创建(通过调用构造方法可以完成对象的创建)。 作用2:对象的初始化(给对象的所有属性赋值就是对象的初始化)。
- 怎么定义构造方法呢?
java
[修饰符列表] 构造方法名(形参列表){
构造方法体;
}
-
构造方法怎么调用呢? 语法:new 构造方法名(实参); 注意:构造方法最终执行结束之后,会自动将创建的对象的内存地址返回。但构造方法体中不需要提供"return 值;"这样的语句。
-
在java语言中,如果一个类没有显示的去定义构造方法,系统会默认提供一个无参数的构造方法。(通常把这个构造方法叫做缺省构造器。)
-
一个类中如果显示的定义了构造方法,系统则不再提供缺省构造器。所以,为了对象创建更加方便,建议把无参数构造方法手动的写出来。
-
在java中,一个类中可以定义多个构造方法,而且这些构造方法自动构成了方法的重载(overload)。
-
构造方法中给属性赋值了?为什么还需要单独定义set方法给属性赋值呢? 在构造方法中赋值是对象第一次创建时属性赋的值。set方法可以在后期的时候调用,来完成属性值的修改。
-
构造方法执行原理? 构造方法执行包括两个重要的阶段:
- 第一阶段:对象的创建
- 第二阶段:对象的初始化 这两个阶段不可颠倒,不可分割。 对象在什么时候创建的? new的时候,会直接在堆内存中开辟空间。然后给所有属性赋默认值,完成对象的创建。(这个过程是在构造方法体执行之前就完成了。)
-
构造代码块 格式:
{}包裹即可。构造代码块什么时候执行,执行几次? 每一次在new的时候,都会先执行一次构造代码块。构造代码块是在构造方法执行之前执行的。 构造代码块有什么用? 如果所有的构造方法在最开始的时候有相同的一部分代码,不妨将这个公共的代码提取到构造代码块当中,这样代码可以得到复用。
理解构造方法和构造代码块对于理解对象的创建和初始化过程非常重要。首先,我们来定义一个简单的类 Car,并演示构造方法的定义、调用和作用:
java
public class Car {
private String brand;
private String color;
private int speed;
// 构造方法1:无参数构造方法(缺省构造器)
public Car() {
System.out.println("无参数构造方法被调用");
// 可以在构造方法中进行初始化工作
brand = "Unknown";
color = "Unknown";
speed = 0;
}
// 构造方法2:带参数的构造方法
public Car(String brand, String color, int speed) {
System.out.println("带参数构造方法被调用");
// 可以在构造方法中进行初始化工作
this.brand = brand;
this.color = color;
this.speed = speed;
}
// 构造代码块
{
System.out.println("构造代码块被执行");
// 可以在构造代码块中执行初始化工作
// 这部分代码会在每次对象创建时执行
// 这里能够使用this,这说明,构造代码块执行之前对象已经创建好了,并且系统也完成了默认赋值。
System.out.println(this.speed);
}
// 演示构造方法的调用和对象的创建
public static void main(String[] args) {
// 调用无参数构造方法创建对象
Car car1 = new Car();
System.out.println("品牌:" + car1.brand + ",颜色:" + car1.color + ",速度:" + car1.speed);
// 调用带参数构造方法创建对象
Car car2 = new Car("Toyota", "Blue", 60);
System.out.println("品牌:" + car2.brand + ",颜色:" + car2.color + ",速度:" + car2.speed);
}
}
上述代码演示了构造方法的定义和调用。其中,无参数构造方法用于创建对象时进行默认初始化,而带参数的构造方法允许在创建对象时传入特定的属性值。
接下来,我们来看一下构造代码块的示例:
java
public class MyClass {
// 构造代码块
{
System.out.println("构造代码块被执行");
// 这里可以进行初始化工作
}
// 构造方法
public MyClass() {
System.out.println("构造方法被调用");
}
public static void main(String[] args) {
MyClass obj1 = new MyClass();
MyClass obj2 = new MyClass();
}
}
在这个示例中,构造代码块会在每次对象创建时执行,用于执行初始化工作。无论是通过哪个构造方法创建对象,构造代码块都会在构造方法之前执行。
6.this关键字进阶

this代表的是当前对象。static的方法中没有当前对象。所以static的方法中不能使用this。

this用法总结:
-
引用当前对象的成员变量 :通过
this关键字可以引用当前对象的成员变量,尤其在成员变量与局部变量同名的情况下,可以使用this关键字明确指定成员变量。 -
引用当前对象的方法 :在非静态方法中,可以使用
this关键字来调用当前对象的其他方法,使代码更加清晰。 -
在构造方法中调用其他构造方法 :通过
this()语法可以在构造方法中调用当前类的其他构造方法,以避免代码重复。
6.1. 引用当前对象的成员变量:
java
public class Example {
private int number;
public Example(int number) {
// 使用this引用当前对象的成员变量
this.number = number;
}
public void printNumber() {
// 使用this引用当前对象的成员变量
System.out.println("Number: " + this.number);
}
public static void main(String[] args) {
Example example = new Example(10);
example.printNumber();
}
}
6.2. 引用当前对象的方法:
java
public class Example {
private String message;
public Example(String message) {
this.message = message;
}
public void displayMessage() {
System.out.println("Message: " + this.message);
}
public void showMessage() {
// 使用this引用当前对象的方法
this.displayMessage();
}
public static void main(String[] args) {
Example example = new Example("Hello");
example.showMessage();
}
}
6.3. 在构造方法中调用其他构造方法:
java
public class Example {
private int number;
private String message;
public Example() {
// 调用带参数的构造方法
this(0, "Default");
}
public Example(int number, String message) {
this.number = number;
this.message = message;
}
public void printInfo() {
System.out.println("Number: " + this.number + ", Message: " + this.message);
}
public static void main(String[] args) {
Example example1 = new Example();
example1.printInfo();
Example example2 = new Example(10, "Hello");
example2.printInfo();
}
}
7.static关键字
- static翻译为静态的
- static修饰的变量:静态变量
- static修饰的方法:静态方法
- 所有static修饰的,访问的时候,直接采用"类名.",不需要new对象。
- 什么情况下把成员变量定义为静态成员变量? 当一个属性是对象级别的,这个属性通常定义为实例变量。(实例变量是一个对象一份。100个对象就应该有100个空间) 当一个属性是类级别的(所有对象都有这个属性,并且这个属性的值是一样的),建议将其定义为静态变量,在内存空间上只有一份。节省内存开销。 这种类级别的属性,不需要new对象,直接通过类名访问。
- 静态变量存储在哪里?静态变量在什么时候初始化?(什么时候开辟空间) JDK8之后:静态变量存储在堆内存当中。 类加载时初始化。
java
public class Chinese {
String idCard;
String name;
static String country = "中国";
public Chinese(String idCard, String name) {
this.idCard = idCard;
this.name = name;
}
public void display() {
System.out.println("身份证号:" + this.idCard + ",姓名:" + this.name + ",国籍:" + this.country);
}
public static void test() {
System.out.println("静态方法test执行了");
// 这个不行
//display();
//System.out.println(name);
// 这些可以
System.out.println(Chinese.country);
System.out.println(country); // 在同一个类中,类名. 可以省略。
Chinese.test2();
test2();
}
public static void test2() {
}
}

7.1静态代码块
静态代码块是在类加载时执行的特殊代码块,用于在类加载时进行一些初始化操作。下面是关于静态代码块的详细讲解:
7.1.1 语法格式:
java
static {
// 静态代码块中的代码
}
7.1.2 执行时机和次数:
- 静态代码块在类加载时执行,且只执行一次。即使没有创建对象,静态代码块也会执行。
7.1.3 可以编写多个静态代码块:
- 静态代码块可以按照自上而下的顺序依次执行。
7.1.4. 使用场景:
- 当需要在类加载时执行一些特定的操作时,可以将代码写入静态代码块中。
- 典型的应用场景包括:初始化静态变量、加载驱动程序、记录日志等。
java
public class StaticTest01 {
// 实例方法
public void doSome() {
System.out.println(name);
}
// 实例变量
String name = "zhangsan";
// 静态变量
static int i = 100;
// 静态代码块1
static {
// 在静态代码块中,可以直接访问静态变量,但不能访问实例变量。
System.out.println(i); // 输出:100
System.out.println("静态代码块1执行了");
System.out.println("xxxx-xx-xx xx:xx:xx 000 -> StaticTest01.class完成了类加载!");
}
// 静态变量
static int j = 10000;
// 静态代码块2
static {
System.out.println("静态代码块2执行了");
}
public static void main(String[] args) {
System.out.println("main execute!");
}
// 静态代码块3
static {
System.out.println("静态代码块3执行了");
}
}
执行过程:
- 类加载器加载
StaticTest01类。 - 静态变量
i被初始化为100。 - 第一个静态代码块执行,输出
"静态代码块1执行了",并执行其中的代码。 - 静态变量
j被初始化为10000。 - 第二个静态代码块执行,输出
"静态代码块2执行了"。 main方法被调用,程序执行结束。
静态代码块在类加载时执行,并且只执行一次。它可以用于执行类的初始化操作,例如初始化静态变量、加载驱动程序等。
8.单例模式
单例模式是一种设计模式,确保类只有一个实例,并提供全局访问点。
8.1饿汉式单例模式
实现步骤:
- 私有化构造方法:防止外部直接通过构造方法创建对象。
- 提供静态方法获取实例:通过一个静态方法返回该类的实例。
- 定义静态变量并初始化:在类加载时初始化静态变量,确保只有一个实例存在。
java
// Singleton.java
public class Singleton {
// 第三步:定义静态变量,在类加载的时候,初始化静态变量。(只初始化一次)
private static Singleton instance = new Singleton();
// 第一步:构造方法私有化,防止外部直接通过构造方法创建对象。
private Singleton() {}
// 第二步:提供一个静态方法获取实例。
public static Singleton getInstance() {
return instance;
}
}
// SingletonTest01.java
public class SingletonTest01 {
public static void main(String[] args) {
// 通过静态方法获取实例
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
Singleton s3 = Singleton.getInstance();
// 判断是否为同一个实例
System.out.println(s1 == s2); // true
System.out.println(s3 == s2); // true
}
}
执行过程:
- 类加载器加载
Singleton类。 - 静态变量
instance被初始化为单例实例。 - 在
main方法中通过静态方法Singleton.getInstance()获取实例。 - 由于静态变量
instance在类加载时就已经初始化,因此返回的是同一个实例。 - 判断
s1 == s2和s3 == s2,都为true,说明它们是同一个实例。
注意事项:
- 饿汉式单例模式在类加载时就创建了实例,可能会造成资源浪费。
- 在多线程环境下,需考虑线程安全问题。
- 饿汉式单例模式:类加载时对象就创建好了。不管这个对象用还是不用。提前先把对象创建好。
这段代码展示了懒汉式单例模式的实现。下面是代码的解释和说明:
8.2懒汉式单例模式。
实现步骤:
- 私有化构造方法:防止外部直接通过构造方法创建对象。
- 提供静态方法获取实例:通过一个静态方法返回该类的实例。
- 定义静态变量但不初始化 :静态变量
s初始值为null,在第一次调用时才创建对象。
java
public class Singleton {
// 第三步:提供一个静态变量,但是这个变量值为null。
private static Singleton instance;
// 第一步:构造方法私有化,防止外部直接通过构造方法创建对象。
private Singleton() {}
// 第二步:提供一个静态方法获取实例。
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
System.out.println("对象创建了"); // 只会在第一次创建对象时输出
}
return instance;
}
}
执行过程:
- 第一次调用
getInstance()方法时,instance为null,创建一个新的Singleton对象并赋值给instance。 - 后续再次调用
getInstance()方法时,由于instance已经不为null,直接返回之前创建的实例。
注意事项:
- 懒汉式单例模式延迟了对象的创建时间,只有在第一次使用时才创建对象,节省了资源。
- 在多线程环境下,需要考虑线程安全问题,可以通过加锁来解决。
9.OOP三大特征之继承
在面向对象编程中,继承是一种重要的概念,它允许一个类(子类)继承另一个类(父类)的属性和方法。下面是关于继承的详细讲解:
9.1 定义继承关系:
- 在 Java 中,使用关键字
extends来定义一个类继承另一个类。 - 子类(派生类)继承了父类(基类)的属性和方法。
java
// Person.java
public class Person {
String name;
int age;
boolean sex;
// 省略 getter 和 setter 方法
public void eat(){
System.out.println(name + "正在吃饭");
}
public void run(){
System.out.println(name + "正在跑步,锻炼身体");
}
}
// Student.java
public class Student extends Person{
String course;
// 省略 getter 和 setter 方法
public void study(){
System.out.println(this.name + "正在努力的学习!");
}
}
// Teacher.java
public class Teacher extends Person{
double sal;
// 省略 getter 和 setter 方法
public void teach(){
System.out.println(name + "正在认真的授课");
}
}
执行过程:
Student类和Teacher类都使用extends关键字继承了Person类。- 因此,
Student类和Teacher类都拥有了Person类中的name、age和sex属性,以及eat()和run()方法。 Student类额外添加了course属性和study()方法。Teacher类额外添加了sal属性和teach()方法。
Java不支持多继承,不能同时直接继承多个类。只能"直接"继承1个类。单继承。 
Java语言中,一个类没有显示的继承任何类,默认继承Object。 这段代码演示了Java中类的继承关系以及默认继承根类 Object。以下是对代码的解释:
9.2默认继承根类 Object:
- 如果一个类没有显式地继承任何类,那么它默认继承根类
Object。 Object是 Java JDK 类库中的根类,位于java.lang包中。
java
class E {
// 类 E 没有显式地继承任何类,因此默认继承 Object 类。
}
class F extends E {
// 类 F 继承了类 E。
}
public class Test2 {
public static void main(String[] args) {
// 创建 E 类的对象
E e = new E();
// 因为 E 类默认继承 Object,所以可以调用 Object 类中的方法
// 打印 e 对象的字符串表示形式
String s = e.toString();
System.out.println(s);
// 创建 F 类的对象
F f = new F();
// 因为 F 类继承了 E 类,而 E 类继承了 Object 类,所以也可以调用 Object 类中的方法
// 打印 f 对象的字符串表示形式
System.out.println(f.toString());
}
}
输出结果:
kotlin
com.rainsoul.oop.oop17.E@2d98a335
com.rainsoul.oop.oop17.F@6996dbf2
- 因为类
E和类F都继承了Object类,所以它们都可以调用Object类中定义的方法,比如toString()方法。 - 默认情况下,
toString()方法返回的是对象的类名和内存地址的十六进制表示形式。
9.3方法覆盖
回顾方法重载 overload
- 什么时候考虑使用方法重载? 在一个类中,如果功能相似,可以考虑使用方法重载。 这样做的目的是:代码美观,方便编程。
- 当满足什么条件的时候构成方法重载? 条件1:在同一个类中。 条件2:相同的方法名。 条件3:不同的参数列表:类型,个数,顺序
- 方法重载机制属于编译阶段的功能。(方法重载机制是给编译器看的。)
方法覆盖/ override/ 方法重写/ overwrite
- 什么时候考虑使用方法重写? 当从父类中继承过来的方法,无法满足子类的业务需求时。
- 当满足什么条件的时候,构成方法重写? 条件1:方法覆盖发生在具有继承关系的父子类之间。 条件2:具有相同的方法名(必须严格一样) 条件3:具有相同的形参列表(必须严格一样) 条件4:具有相同的返回值类型(可以是子类型) 3. 关于方法覆盖的细节:
3.1 当子类将父类方法覆盖之后,将来子类对象调用方法的时候,一定会执行重写之后的方法。 3.2 在java语言中,有一个注解,这个注解可以在编译阶段检查这个方法是否是重写了父类的方法。
Override注解是JDK5引入,用来标注方法,被标注的方法必须是重写父类的方法,如果不是重写的方法,编译器会报错。Override注解只在编译阶段有用,和运行期无关。
3.3 如果返回值类型是引用数据类型,那么这个返回值类型可以是原类型的子类型
3.4 访问权限不能变低,可以变高。 3.5 抛出异常不能变多,可以变少。(后面学习异常的时候再说。)
3.6 私有的方法,以及构造方法不能继承,因此他们不存在方法覆盖。
3.7 方法覆盖针对的是实例方法。和静态方法无关。(讲完多态再说。)
3.8 方法覆盖针对的是实例方法。和实例变量没有关系。
java
// OverrideTest01.java
/**
* 主类,用于演示方法覆盖(override)的示例。
*/
public class OverrideTest01 {
public static void main(String[] args) {
// 创建鸟儿对象
Bird b = new Bird();
// 调用方法
b.eat(); // 调用的是从 Animal 类继承的 eat 方法
b.move(); // 调用的是 Bird 类中覆盖了的 move 方法
}
}
java
// Animal.java
/**
* 动物类,包含吃和移动方法。
*/
public class Animal {
/**
* 动物吃的行为。
*/
public void eat(){
System.out.println("动物在吃东西");
}
/**
* 动物移动的行为。
*/
public void move(){
System.out.println("动物在移动");
}
/**
* 根据参数返回一个对象,用于演示方法覆盖中返回值类型的灵活性。
*
* @param a 参数a
* @param b 参数b
* @return 返回一个对象
*/
public Object getObj(long a, String b){
return null;
}
}
java
// Bird.java
/**
* 鸟类,继承自动物类,用于演示方法覆盖。
*/
public class Bird extends Animal{
/**
* Bird 对继承过来的 move() 方法不满意,因此对其进行了覆盖。
*/
@Override
public void move(){
System.out.println("鸟儿在飞翔!");
}
/**
* 从动物类继承过来的 getObj 方法被 Bird 类重写,返回值类型变为 String。
*
* @param a 参数a
* @param b 参数b
* @return 返回一个字符串
*/
@Override
public String getObj(long a, String b){
return "";
}
}
10.多态
10.1向上转型和向下转型
关于基本数据类型之间的类型转换:
- 第一种:小容量转换成大容量,叫做自动类型转换。
int i = 100; long x = i; - 第二种:大容量转换成小容量,不能自动转换,必须添加强制类型转换符才行。叫做强制类型转换。 int y = (int) x; 除了基本数据类型之间的类型转换之外,对于引用数据类型来说,也可以进行类型转换。 只不过不叫做自动类型转换和强制类型转换。我们一般称为向上转型和向下转型。 关于Java语言中的向上转型和向下转型:
- 向上转型(upcasting):子 ---> 父 (可以等同看做自动类型转换。)
- 向下转型(downcasting):父 ---> 子 (可以等同看做强制类型转换。) 不管是向上还是向下转型,两种类型之间必须要有继承关系,编译器才能编译通过。这是最基本的大前提。
java
public class TypeConversion {
public static void main(String[] args) {
// 自动类型转换:小容量转换成大容量
int i = 100;
long x = i; // 自动将int类型转换成long类型
// 强制类型转换:大容量转换成小容量
long y = 1000L;
int z = (int) y; // 强制将long类型转换成int类型
// 向上转型:子类对象转换成父类对象
Animal animal = new Dog(); // 向上转型,Dog对象转换成Animal对象
// 向下转型:父类对象转换成子类对象
Animal animal2 = new Dog();
Dog dog = (Dog) animal2; // 向下转型,Animal对象转换成Dog对象
}
}
// 父类
class Animal {
// 父类方法
}
// 子类
class Dog extends Animal {
// 子类方法
}
在这个例子中,我们演示了基本数据类型之间的自动类型转换和强制类型转换,以及引用数据类型之间的向上转型和向下转型。请注意,在向下转型时,需要确保父类对象实际上是子类对象的实例,否则会出现运行时异常。
plaintext
父类(Animal) 子类(Dog)
↑ ↑
| |
向上转型 向下转型
| |
Animal animal = new Dog(); Dog dog = (Dog) animal;
向上转型(upcasting):
- 子 --> 父
- 也可以等同看做自动类型转换
- 前提:两种类型之间要有继承关系
- 父类型引用指向子类型对象。这个就是多态机制最核心的语法。
10.2多态的概念
java程序包括两个重要的阶段:
- 第一阶段:编译阶段
在编译的时候,编译器只知道a2的类型是Animal类型。因此在编译的时候就会去Animal类中找move()方法。找到之后,绑定上去,此时发生静态绑定。能够绑定成功,表示编译通过。 - 第二阶段:运行阶段
在运行的时候,堆内存中真实的java对象是Cat类型。所以move()的行为一定是Cat对象发生的。 因此运行的时候就会自动调用Cat对象的move()方法。这种绑定称为运行期绑定/动态绑定。
因为编译阶段是一种形态,运行的时候是另一种形态。因此得名:多态。
java
package com.rainsoul.oop.oop19;
/**
* Animal 类表示动物,定义了动物的一般行为。
*/
public class Animal {
/**
* 动物移动的方法。
*/
public void move(){
System.out.println("动物在移动");
}
/**
* 动物进食的方法。
*/
public void eat(){
System.out.println("正在吃东西");
}
}
java
package com.rainsoul.oop.oop19;
/**
* Bird 类表示鸟类,继承自 Animal 类。
*/
public class Bird extends Animal{
/**
* 重写了动物移动的方法,表示鸟儿在飞翔。
*/
@Override
public void move() {
System.out.println("鸟儿在飞翔");
}
/**
* 鸟儿特有的歌唱方法。
*/
public void sing(){
System.out.println("鸟儿在歌唱!");
}
}
java
package com.rainsoul.oop.oop19;
/**
* Cat 类表示猫类,继承自 Animal 类。
*/
public class Cat extends Animal{
/**
* 重写了动物移动的方法,表示猫在走猫步。
*/
@Override
public void move() {
System.out.println("猫在走猫步");
}
/**
* 猫特有的抓老鼠方法。
*/
public void catchMouse(){
System.out.println("猫在抓老鼠");
}
}
java
public class Test01 {
public static void main(String[] args) {
Animal a2 = new Cat();
Animal a3 = new Bird();
// 调用动物的移动方法,由于动态绑定,会执行对应子类的方法。
a2.move();
/*
* 下面的代码是编译错误,因为编译器只知道a2是Animal类型,去Animal类中找
* catchMouse()方法了,结果没有找到,无法完成静态绑定,编译报错。
*/
//a2.catchMouse();
/*
* 假如现在就是要让a2去抓老鼠,怎么办?
* 向下转型:downcasting(父--->子)
* 什么时候我们会考虑使用向下转型?
* 当调用的方法是子类中特有的方法。
*/
Cat c2 = (Cat) a2;
c2.catchMouse();
// 多态
Animal x = new Cat();
// 向下转型,为了避免ClassCastException异常,使用instanceof运算符进行判断
if (x instanceof Bird) {
System.out.println("=========================");
Bird y = (Bird) x;
}
// instanceof 运算符的语法规则:
// 1. instanceof运算符的结果一定是:true/false
// 2. 语法格式:
// (引用 instanceof 类型)
// 3. 例如:
// (a instanceof Cat)
// true表示什么?
// a引用指向的对象是Cat类型。
// false表示什么?
// a引用指向的对象不是Cat类型。
// 多态
Animal a = new Bird();
a.eat();
// 需求:程序运行阶段动态确定对象
// 如果对象是Cat,请抓老鼠。
// 如果对象是Bird,请唱歌。
if (a instanceof Cat) {
Cat cat = (Cat) a;
cat.catchMouse();
} else if (a instanceof Bird) {
Bird bird = (Bird) a;
bird.sing();
}
}
}
10.3 总结
多态的基础语法主要涉及以下几个方面:
-
继承关系: 多态的实现基础是类与类之间的继承关系。在继承关系中,子类可以继承父类的属性和方法。
-
方法重写(覆盖): 子类可以重写(覆盖)父类的方法,以满足自身的业务需求。重写的方法必须与父类方法具有相同的签名(方法名、参数列表和返回类型)。
-
向上转型(upcasting): 向上转型是指将子类的实例赋值给父类类型的引用变量。这样的赋值操作是自动进行的,无需强制类型转换。通过向上转型,可以实现多态性,使得父类类型的引用变量可以指向子类对象。
-
向下转型(downcasting): 向下转型是指将父类类型的引用变量转换为子类类型的引用变量。这种转换需要显式地进行,并且在运行时可能会抛出
ClassCastException异常。向下转型可以让我们调用子类特有的方法。 -
instanceof运算符: instanceof运算符用于判断一个对象是否是某个类的实例。它的语法格式为
(对象 instanceof 类型),返回结果为true或false。通过使用instanceof运算符,可以在向下转型之前先判断对象的类型,避免发生类型转换异常。 -
动态绑定(运行期绑定): 在多态中,方法的调用是在运行时确定的,而不是在编译时确定的。这意味着,即使使用父类类型的引用变量来调用方法,实际执行的是子类对象的方法。这种绑定方式称为动态绑定或运行期绑定。
10.4多态的优势
-
代码简洁: 多态能够通过统一的接口来处理不同类型的对象,从而简化了代码的编写。在代码中,不再需要为每个具体的对象编写特定的处理逻辑,而是通过多态机制实现统一的处理方式。
-
提高灵活性: 多态使得程序更加灵活,能够适应不同类型的变化。通过多态,可以在不改变代码结构的情况下,轻松地引入新的子类,扩展程序的功能。
-
提高可扩展性: 多态支持基于抽象类或接口编程,而不是具体类,从而降低了代码之间的耦合度。这使得程序更容易扩展和维护,因为可以通过扩展抽象类或接口来添加新的功能,而不会影响现有代码的稳定性。
-
提高代码复用性: 多态能够使得代码中的通用部分得到更多的复用。通过将相同的行为封装在抽象类或接口中,不同的子类可以共享这些行为,从而避免了重复编写相似的代码。
java
// 抽象动物类
abstract class Animal {
// 抽象方法:吃
public abstract void eat();
}
// 猫类,继承自动物类
class Cat extends Animal {
// 实现吃方法
@Override
public void eat() {
System.out.println("猫吃鱼");
}
}
// 狗类,继承自动物类
class Dog extends Animal {
// 实现吃方法
@Override
public void eat() {
System.out.println("狗啃骨头");
}
}
// 主人类
class Master {
// 喂食方法,利用多态
public void feed(Animal animal) {
animal.eat(); // 调用动物的吃方法,具体吃什么由实际的动物类决定
}
}
// 测试类
public class TestPolymorphism {
public static void main(String[] args) {
// 创建主人对象
Master master = new Master();
// 创建猫对象和狗对象
Animal cat = new Cat();
Animal dog = new Dog();
// 主人喂猫和狗,利用多态
master.feed(cat); // 输出:猫吃鱼
master.feed(dog); // 输出:狗啃骨头
}
}
通过多态,主人类的feed方法可以接收不同类型的动物对象,并统一进行喂食操作。这样,在新增其他类型的动物时,不需要修改主人类的feed方法,实现了代码的灵活性和可扩展性。
10.5静态方法和实例变量没有覆盖
方法覆盖针对的是实例方法。和静态方法无关。方法的覆盖和多态机制联合起来才有意义。
这段代码展示了关于静态方法和实例变量的一些特性,让我们来总结一下:
-
静态方法的覆盖: 静态方法是属于类的方法,与对象无关,因此不存在方法覆盖的概念。在子类中定义与父类相同的静态方法,实际上是隐藏了父类的静态方法,并非方法的覆盖。
-
静态方法和多态: 多态是针对实例方法的,在多态中,父类引用可以指向子类对象。但对于静态方法,调用时直接通过类名来调用,不会受到多态的影响,因此无论引用指向哪个对象,调用的都是定义该静态方法的类的版本。
-
实例变量的覆盖: 实例变量是属于对象的属性,对于同名的实例变量,子类可以对其进行覆盖。但在实例方法中访问这个变量时,由于Java是根据引用的类型来绑定的,因此编译器会根据引用的类型选择变量,而不会根据对象的实际类型选择。
下面是代码中的一些示例:
java
public class Animal {
public static void test() {
System.out.println("Animal's test method invoked");
}
}
public class Cat extends Animal {
public static void test() {
System.out.println("Cat's test method invoked");
}
}
public class Test {
public static void main(String[] args) {
Animal.test(); // 输出:Animal's test method invoked
Cat.test(); // 输出:Cat's test method invoked
// 多态
Animal a = new Cat();
a.test(); // 输出:Animal's test method invoked
}
}
public class Test2 {
public static void main(String[] args) {
// 多态
A a = new B();
// 输出:张三
// 实例变量不存在覆盖这一说。
// a.name编译阶段绑定的是A类的name属性,运行的时候也会输出A类的name属性值。
System.out.println(a.name);
// 没有使用多态
B b = new B();
// 输出:李四
System.out.println(b.name);
}
}
class A {
String name = "张三";
}
class B extends A {
String name = "李四";
}
综上所述,静态方法在多态中调用时会绑定到定义该方法的类的版本,而实例变量的访问则是根据引用的类型进行绑定。
chatgpt解释:
!chatgpt\] 静态方法和实例变量之所以没有覆盖(即不具有方法覆盖和变量覆盖的特性),主要是因为它们的访问方式和绑定方式与实例方法和实例变量不同。下面分别解释一下: 1. **静态方法:** * 静态方法是属于类的方法,不是属于对象的方法。在调用静态方法时,不需要创建类的实例,而是直接通过类名来调用。因此,它们不受多态的影响,无论通过哪个子类的引用来调用,都会调用定义在父类中的静态方法。因此,静态方法不存在覆盖的概念,子类中定义与父类相同的静态方法实际上是隐藏了父类的静态方法。 2. **实例变量:** * 实例变量是属于对象的属性,它们的访问和绑定是根据对象的类型进行的。但是在Java中,子类中的实例变量不能完全覆盖父类中的实例变量,而是属于隐藏的关系。当子类中定义了与父类相同名称的实例变量时,会隐藏父类中的实例变量,但是在实际使用中,编译器会根据引用的类型来选择变量,而不会根据对象的实际类型来选择。 综上所述,静态方法和实例变量的访问方式和绑定方式不同于实例方法和实例变量,因此它们不具有方法覆盖和变量覆盖的特性。