😀 万物皆对象!!!
📝 面向对象
面向过程:一步一步,分步骤执行。(比如在家想吃某一个菜,需要先去买食材,进行步骤处理食材烹煮等)
面向对象:模块化的,底层还是面向过程。(但是如果是到外面餐厅想吃同一道菜,直接点菜单,由服务员告诉厨师就行。)
类和对象
类是对象的模板,定义了对象的属性和方法
对象是类的实例,具有具体的状态和行为
类是Java面向对象编程的基本单位,它是对象的模板或蓝图。
类定义了一组对象的共同特征(属性)和行为(方法)。例如:人的肤色,黄色or白色等特征属性,人能够吃饭睡觉等行为方法。
类描述了一系列具有相同特征和行为的对象,从宽泛的概念上来说,类其实就是一种自定义的数据类型。Car其实就是抽象数据类型,封装了与车相关的数据和操作。
抽象的理解
万物皆对象。其中的“万物”指的是 Java 中的所有类,而这些类都是 Object 类的子类。
一段程序实际上就是多个对象通过发送消息的方式来告诉彼此该做什么。
通过组合的方式,可以将多个对象封装成其他更为基础的对象。
对象是通过类实例化的。
同一类型的对象可以接收相同的消息。
类的组成部分:
-
属性(字段): 描述对象的状态
-
方法: 描述对象的行为
-
构造方法: 用于创建和初始化对象 Java中有默认的无参构造方法
public class Car {
// 属性 成员变量(在类内部但在方法外部,方法内部的叫临时变量。)
// 又称实例变量 在编译时不占用内存空间,在运行时获取内存,只有在对象实例化后,字段才会获取到内存
private String brand;
private String color;
private int speed;
// 构造方法
public Car(String brand, String color) {
this.brand = brand;
this.color = color;
this.speed = 0;
}
// 方法
public void accelerate(int increment) {
speed += increment;
}
public void brake() {
speed = 0;
}
}
package(包),Java中的一种名字空间。一个类总是属于某一个包的。
大多数时候所见的类名都是缩写,如Car,Person等,完整的类名应该是:包名.类名,如 test.Car ,Test.Person等。
在Java 虚拟机执行的时候,JVM 只看完整类名,因此,只要包名不同,类就不同。
包可以是多层结构,用
.
隔开。例如:java.util
。特别注意:
包没有父子关系。java.util和java.util.zip是不同的包,两者没有任何继承关系。
未加访问控制修饰符修饰的方法或者属性,默认的作用域就是包作用域。
编译器会自动帮我们做两个 import 动作:
默认自动
import
当前package
的其他class
;默认自动
import java.lang.*
,自动导入的是java.lang包,但类似java.lang.reflect这些包仍需要手动导入。包的命名使用倒置的域名来确保唯一性,org.apache.commons。
对象是类的实例,它是类的具体表现。
每个对象都有自己的状态(属性值)和行为(可以调用的方法)。
创建对象:
-
使用new关键字
-
调用构造方法
Car car = new Car("Toyota", "Red");
car 被称为对象 Car 的引用变量,见下图:
所有对象在创建的时候都会在堆内存中分配空间。
通过对象的引用变量,使用点(.)操作符来访问对象的属性和方法,也可以直接对字段进行初始化。
car.accelerate(20);
car.speed = 30;
不可变对象
不可变对象(immutable objects)是指一旦创建就不能被修改的对象。
1、定义不可变对象
-
类本身必须是
final
,以防止被继承和修改。常见不可变类就是 String 类。 -
所有字段都应该是
private
和final
,确保它们在初始化后不会被改变。 -
没有提供任何可以修改对象状态的方法,包括setter方法。
-
确保类中的可变对象不会被外部直接访问或修改(通过深拷贝或者返回不可变的副本)。
-
在构造函数中完成所有字段的初始化。
public final class ImmutableBook {
private final String title;
private final String author;
private final String isbn;
// 构造函数
public ImmutableBook(String title, String author, String isbn) {
if (title == null || author == null || isbn == null) {
throw new IllegalArgumentException("Arguments cannot be null");
}
this.title = title;
this.author = author;
this.isbn = isbn;
}
// 获取书名
public String getTitle() {
return title;
}
// 获取作者
public String getAuthor() {
return author;
}
// 获取ISBN号
public String getIsbn() {
return isbn;
}
// 重写 toString 方法
@Override
public String toString() {
return "ImmutableBook{" +
"title='" + title + '\'' +
", author='" + author + '\'' +
", isbn='" + isbn + '\'' +
'}';
}
// 重写 equals 方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ImmutableBook that = (ImmutableBook) o;
return title.equals(that.title) && author.equals(that.author) && isbn.equals(that.isbn);
}
// 重写 hashCode 方法
@Override
public int hashCode() {
return Objects.hash(title, author, isbn);
}
}
public class Main {
public static void main(String[] args) {
// 创建 ImmutableBook 对象
ImmutableBook book1 = new ImmutableBook("Effective Java", "Joshua Bloch", "978-0134685991");
ImmutableBook book2 = new ImmutableBook("Clean Code", "Robert C. Martin", "978-0132350884");
// 输出书的信息
System.out.println(book1);
System.out.println(book2);
// 尝试修改书的信息(这是不可能的,因为没有提供 setter 方法)
// book1.setTitle("New Title"); // 编译错误:方法不存在
// 比较两个书对象
ImmutableBook anotherBook1 = new ImmutableBook("Effective Java", "Joshua Bloch", "978-0134685991");
System.out.println(book1.equals(anotherBook1)); // 输出:true
}
}
2、不可变对象的好处
-
线程安全:不可变对象天生就是线程安全的,因为它们的状态不能被修改。
-
简单性:由于对象的状态一旦创建就不能改变,调试和理解代码变得更加简单。
-
安全性:不可变对象可以防止外部代码不小心修改对象状态,提高了代码的安全性。
变量
变量是用于存储数据的容器,由数据类型(基本数据类型和引用数据类型)和变量名组成。
变量按照作用域的范围又可分为三种类型:局部变量,成员变量和静态变量。
局部变量
在方法体内声明的变量被称为局部变量,该变量只能在该方法内使用,类中的其他方法并不知道该变量。
-
定义在方法、构造器或语句块内。
-
作用域仅限于定义它的方法、构造器或语句块。
-
生命周期从声明开始到方法、构造器或语句块结束。
-
必须先初始化后使用。
-
访问修饰符不能用于局部变量。
-
局部变量是在栈上分配的。
public void exampleMethod() {
int localVar = 10; // 局部变量
// localVar 只在这个方法内有效
}
成员变量(实例变量)
在类内部但在方法体外声明的变量称为成员变量,或者实例变量,或者字段。之所以称为实例变量,是因为该变量只能通过类的实例(对象)来访问。
-
定义在类中,但在方法、构造方法和语句块之外。
-
作用域为整个类,访问修饰符可以修饰成员变量。
-
生命周期与对象实例相同,在对象创建的时候创建,在对象被销毁的时候销毁。
-
成员变量具有默认值。数值型变量的默认值是 0,布尔型变量的默认值是 false,引用类型变量的默认值是 null。
-
成员变量可以声明在使用前或者使用后。
public class ExampleClass {
private int memberVar; // 成员变量
// memberVar 在整个类中都可访问
}
静态变量(类变量)
通过 static 关键字声明的变量被称为静态变量(类变量),它可以直接被类访问。
-
使用static关键字声明,但必须在方法构造方法和语句块之外。
-
作用域为整个类,可通过
类名.静态变量
直接访问,不需要创建类的实例。 -
静态变量在程序开始时创建,在程序结束时销毁,生命周期与类相同,整个程序运行期间都存在。
-
有默认初始值,数值型变量的默认值是 0,布尔型变量的默认值是 false,引用类型变量的默认值是 null。
public class ExampleClass {
public static int staticVar; // 静态变量
// staticVar 可通过 ExampleClass.staticVar 访问
}
常量
常量是在程序运行过程中值不能被改变的量。在Java中,使用final关键字来声明常量。常量的值一旦给定就无法改变!
使用场景
-
定义固定值: 如数学常数π,
final double PI = 3.14
-
定义配置信息: 如数据库连接URL,
final String DATABASE_URL = "jdbc:mysql://localhost:3306/mydb"
final 数据类型 常量名 = 值;
public class Constants {
public static final int MAX_USERS = 100;
}
方法
方法是一段用来完成特定功能的代码块。它用来实现代码的可重用性,我们编写一次方法,并多次使用它。
程序的入口,
main()
方法。
方法声明
方法的声明反映了方法的一些信息,比如说可见性、返回类型、方法名和参数等。
-
修饰符: 如public, private, protected, static等
-
返回类型: 可以是任何数据类型或void(无返回值)
-
方法名: 遵循驼峰命名法,通常以动词开头
-
参数列表: 方法接收的输入,可以有多个参数,也可以没有
-
方法体: 实现方法功能的代码
-
return语句: 返回方法的结果(如果有的话)
修饰符 返回类型 方法名(参数列表) {
// 方法体
return 返回值; // 如果有返回值的话
}
方法的类型
-
实例方法: 没有使用 static 关键字修饰,但在类中声明的方法被称为实例方法,在调用实例方法之前,必须创建类的对象。
特点:
-
可以直接访问实例变量(字段)和实例方法。
-
可以使用 this 关键字。
-
getter 、setter方法。
public class Car { private int n; private String color; public void setColor(String color) { this.color = color; //直接访问实例变量 } public String getColor() { return this.color; } // 实例方法 public void haveCar() { n++; // 直接访问并修改实例变量 System.out.println("car is"+n); } } // 使用 Car myCar = new Car(); myCar.setColor("红色");
-
-
静态方法(类方法): 使用static修饰,可以通过类名直接调用,也可以通过实例调用(但不推荐)。
特点:
-
不能直接访问实例变量和实例方法。(实例变量属于对象,每个对象都有自己的副本,静态方法在被调用时,可能还没有任何该类的实例被创建。)
-
不能使用 this 关键字。
-
可以访问静态变量和其他静态方法。
public class MathUtils { public static int add(int a, int b) { return a + b; } } // 使用 int result = MathUtils.add(5, 3);
-
-
抽象方法:一个没有方法体(实现)的方法,只有方法的声明。
它总是在抽象类中声明。这意味着如果类有抽象方法的话,这个类就必须是抽象的。可以使用 abstract 关键字创建抽象方法和抽象类。
特点:
-
使用
abstract
关键字修饰。 -
只能在抽象类或接口中声明。
-
抽象类的子类必须实现所有抽象方法,除非子类也是抽象类。
-
抽象方法不能是私有的(private),因为它需要被子类重写。
-
抽象方法不能是静态的(static),因为静态方法不能被重写。
-
抽象方法不能是final的,因为final方法不能被重写。
abstract 返回类型 方法名(参数列表); public abstract class Animal { public abstract void makeSound(); } public class Dog extends Animal { @Override public void makeSound() { System.out.println("狗叫:汪汪!"); } } public class Cat extends Animal { @Override public void makeSound() { System.out.println("猫叫:喵喵!"); } }
-
本地方法(native)
本地方法是一种Java方法,其实现是用另一种编程语言(通常是C或C++)编写的。
目的:
-
提高性能:某些操作用C/C++实现可能比Java更高效。
-
访问系统级功能:某些底层系统功能在Java中无法直接访问。
-
重用已有代码:利用现有的C/C++库。
native 语法:
-
①、修饰方法的位置必须在返回类型之前,和其余的方法控制符前后关系不受限制。
-
②、不能用 abstract 修饰,也没有方法体,也没有左右大括号。
-
③、返回值可以是任意类型。
**JNI(Java Native Interface)**允许 Java 代码和其他语言编写的代码进行交互。通过 JNI,我们就可以通过 Java 程序(代码)调用到操作系统相关的技术实现的库函数,从而与其他技术和系统交互;同时其他技术和系统也可以通过 JNI 提供的相应原生接口调用 Java 应用系统内部实现的功能。
本地方法调用流程图:
实现步骤:
1、声明本地方法: 首先,在Java类中声明本地方法。例如,我们创建一个名为NativeExample的类:
public class NativeExample {
// 声明一个本地方法
public native int add(int a, int b);
// 静态代码块,用于加载包含本地方法实现的库
static {
System.loadLibrary("nativelib");
}
public static void main(String[] args) {
NativeExample example = new NativeExample();
int result = example.add(5, 3);
System.out.println("5 + 3 = " + result);
}
}
2、编译Java代码: 使用javac命令编译Java文件
javac NativeExample.java
3、生成C/C++头文件: 使用javah命令生成包含本地方法声明的C头文件,这会生成一个名为NativeExample.h的头文件。
javah -jni NativeExample
4、实现本地方法: 创建一个C文件(例如nativelib.c)来实现本地方法。
#include <jni.h>
#include "NativeExample.h"
JNIEXPORT jint JNICALL Java_NativeExample_add
(JNIEnv *env, jobject obj, jint a, jint b) {
return a + b;
}
5、编译本地库: 将C文件编译成共享库。(Linux)
gcc -shared -fpic -o libnativelib.so -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux nativelib.c
6、运行Java程序: 确保生成的库文件在Java的库路径中,然后运行Java程序。
java -Djava.library.path=. NativeExample
构造方法
构造方法是一种特殊的方法,用于创建和初始化对象。它在对象被创建时自动调用。平常写代码有时并未写构造方法,那么此时编译器会给这个类提供一个默认的无参构造方法。
基本特征:
-
构造方法的名称必须与类名完全相同。
-
默认构造方法的目的主要是为对象的字段提供默认值。
-
构造方法没有返回类型,甚至不能用void。如果用 void 声明构造方法的话,编译时不会报错,但 Java 会把这个所谓的“构造方法”当成普通方法来处理。
public class Demo { public Demo() { } //普通方法 void Demo() { } }
-
构造方法可以有参数,也可以没有参数。(有参、无参)不过如果没有有参构造方法的话,就需要通过 setter 方法给字段赋值了。
-
构造方法不能被直接调用: 构造方法只能在创建对象时通过new关键字间接调用。
构造方法可以使用这些访问修饰符(public, protected, default, private)但不能是抽象的(abstract)、静态的(static)、最终的(final)、同步的(synchronized)。在实现单例模式时,通常将构造方法设为private。
示例代码:
public class Person {
private String name;
private int age;
// 无参构造方法
public Person() {
}
// 带参数的构造方法
public Person(String name, int age) {
this.name = name;
this.age = age;
}
//构造方法的重载
// 只有name参数的构造方法
public Person(String name) {
this(name, 0); // 调用带两个参数的构造方法
}
// 只有年龄的构造方法
public Student(int age) {
this.age = age;
}
}
构造方法和方法的区别:
代码初始化和构造方法执行顺序比较
代码初始化主要用于设置程序运行所需的初始状态,包括变量赋值、对象创建、集合数组初始化、资源加载(读配置文件等)等,以确保程序能够正确且高效地开始执行。
代码初始化规则:
类实例化的时候执行代码初始化块;
实际上,代码初始化块是放在构造方法中执行的,只不过比较靠前;
代码初始化块里的执行顺序是从前到后的。
代码示例:
public class Car {
Car() {
System.out.println("构造方法");
}
{
System.out.println("代码初始化块");
}
public static void main(String[] args) {
new Car();
}
}
/**
代码初始化块
构造方法
**/
对象在初始化的时候会先调用构造方法,只不过,构造方法在执行的时候会把代码初始化块放在构造方法中其他代码之前,所以就会先看到‘代码初始化块’,后看到了‘’构造方法’。
类继承时的初始化流程
顺序为:
1、父类静态成员初始化
2、子类静态成员初始化
3、父类实例成员和实例初始化块
4、父类构造方法
5、子类实例成员和实例初始化块
6、子类构造方法
class Vehicle {
static String type = "通用车辆";
String brand;
static {
System.out.println("1. Vehicle静态初始化:type = " + type);
}
{
brand = "未知品牌";
System.out.println("3. Vehicle实例初始化:brand = " + brand);
}
Vehicle() {
System.out.println("4. Vehicle构造方法");
brand = "知名品牌";
}
}
class Car extends Vehicle {
static int wheels = 4;
String model;
static {
System.out.println("2. Car静态初始化:wheels = " + wheels);
type = "小汽车";
}
{
model = "基础型号";
System.out.println("5. Car实例初始化:model = " + model);
}
Car() {
System.out.println("6. Car构造方法");
model = "高级型号";
}
public static void main(String[] args) {
Car myCar = new Car();
System.out.println("\n最终状态:");
System.out.println("Vehicle.type = " + Vehicle.type);
System.out.println("Car.wheels = " + Car.wheels);
System.out.println("myCar.brand = " + myCar.brand);
System.out.println("myCar.model = " + myCar.model);
}
}
/**
1. Vehicle静态初始化:type = 通用车辆
2. Car静态初始化:wheels = 4
3. Vehicle实例初始化:brand = 未知品牌
4. Vehicle构造方法
5. Car实例初始化:model = 基础型号
6. Car构造方法
最终状态:
Vehicle.type = 小汽车
Car.wheels = 4
myCar.brand = 知名品牌
myCar.model = 高级型号
**/
静态初始化块在类加载时执行,只会执行一次,并且优先于实例初始化块和构造方法的执行;实例初始化块在每次创建对象时执行,在构造方法之前执行。
抽象类
抽象类用于定义不能实例化的类。抽象类可以包含成员变量、构造函数以及所有类型的成员方法。
抽象类的设计目的是提供一个类层次结构中的基础类,它可以包含抽象方法(未实现的方法)以及具体方法(已实现的方法)。
1. 定义抽象类
使用abstract
关键字来定义抽象类。抽象类不能被实例化,但可以被继承。
关于抽象类的命名,《阿里的 Java 开发手册》上有强调,“抽象类命名要使用 Abstract 或 Base 开头”,这条规约还是值得遵守的,真正做到名如其意。
public abstract class Animal {
// 抽象方法,没有方法体
abstract void makeSound();
// 具体方法,有方法体
void breathe() {
System.out.println("Animal is breathing");
}
}
2. 抽象方法
抽象方法是没有方法体的方法,必须在抽象类中声明。
-
如果一个非抽象子类继承了抽象类,那么该子类必须重写所有从抽象类继承的抽象方法,提供具体实现。
-
如果该子类并没有实现这些抽象方法,那么该子类也必须声明为抽象类。
abstract class Animal {
abstract void makeSound();
}
class Dog extends Animal {
// 实现抽象方法
@Override
void makeSound() {
System.out.println("Bark");
}
}
abstract class Bird extends Animal {
// 没有实现makeSound,所以必须声明为抽象类
}
3、抽象类的构造方法和其他具体方法使用
尽管抽象类不能被直接实例化,但它们可以有构造函数和具体方法。构造函数用于在子类实例化时初始化抽象类中的成员变量,而具体方法可以在抽象类中实现,并在子类中直接使用或重写。
abstract class Animal {
String name;
// 抽象类中的构造函数
Animal(String name) {
this.name = name;
}
// 抽象方法
abstract void makeSound();
// 具体方法
void sleep() {
System.out.println(name + " is sleeping");
}
}
class Dog extends Animal {
Dog(String name) {
super(name);
}
// 实现抽象方法
void makeSound() {
System.out.println(name + " says: Bark");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog("Buddy");
dog.makeSound(); // 输出: Buddy says: Bark
dog.sleep(); // 输出: Buddy is sleeping
}
}
4、抽象类的使用场景-模版方法模式
抽象类通常用作定义模板和默认行为的基础类。它们可以提供部分实现,并强制子类提供特定实现。
abstract class Game {
abstract void initialize();
abstract void startPlay();
abstract void endPlay();
// 模板方法
public final void play() {
initialize();
startPlay();
endPlay();
}
}
class Football extends Game {
void initialize() {
System.out.println("Football Game Initialized!");
}
void startPlay() {
System.out.println("Football Game Started!");
}
void endPlay() {
System.out.println("Football Game Ended!");
}
}
public class Main {
public static void main(String[] args) {
Game game = new Football();
game.play();
}
}
接口
接口(interface)是一种抽象类型,定义了一组抽象方法,类通过实现接口来遵从其规范。接口用于定义类应该具备的一些行为,但不涉及具体实现。
1. 定义接口
使用interface
关键字来定义接口。接口中的方法默认是抽象的,变量默认是public static final
的。
接口中定义的所有变量或者方法,都会自动添加上
public
关键字
public interface Animal {
// 常量
String LED = "LED";
int AGE = 5; // 等同于 public static final int AGE = 5;
void makeSound(); //抽象方法 编译后的字节码 public abstract void makeSound();
//私有方法 私有方法只能在接口内部被调用,用于重用代码。
private void log() {
System.out.println("Logging from interface");
}
//默认方法 默认方法有方法体。实现类可以选择重写这些默认方法。
default void sleep() {
System.out.println("Animal is sleeping");
log(); // 调用私有方法
}
//静态方法 它只能通过接口名来调用,Animal.info()
static void info() {
System.out.println("This is an animal interface");
}
}
2. 实现接口
接口不允许直接实例化,所以一般是类通过implements
关键字实现接口在进行实例化,该类必须实现接口中所有的抽象方法。
public class Dog implements Animal {
public void makeSound() {
System.out.println("Bark");
}
}
//实例化
Animal a = new Dog();
3. 多重继承
Java类不支持多重继承,但接口可以实现多重继承。一个类可以实现多个接口,多个接口可以继承自多个接口。
interface Animal {
void makeSound();
}
interface Pet {
void play();
}
public class Dog implements Animal, Pet {
public void makeSound() {
System.out.println("Bark");
}
public void play() {
System.out.println("Playing fetch");
}
}
同时一个接口可以继承另一个接口(多个接口),子接口继承父接口的抽象方法、默认方法和常量。
interface Animal {
void makeSound();
}
interface Pet extends Animal {
void play();
}
class Dog implements Pet {
public void makeSound() {
System.out.println("Bark");
}
public void play() {
System.out.println("Playing fetch");
}
}
9. 标记接口
标记接口(Marker Interface)是没有方法的接口,用于给类打标签以表示某种特性或行为。
接口可以是空的,既可以不定义变量,也可以不定义方法.
interface Serializable {
// 没有方法,仅用于标记类可被序列化
}
class Data implements Serializable {
int id;
String name;
}
注意点:
**不要在定义接口的时候使用 final 关键字,**因为接口就是为了让子类实现的,而 final 阻止了这种行为。
接口的抽象方法不能是 private、protected 或者 final,否则编译器都会报错。
Java 原则上只支持单一继承,但通过接口可以实现多重继承的目的。
4、抽象类和接口的区别
-
实现方式
-
抽象类:使用
abstract
关键字定义。类使用extends
关键字继承抽象类。 -
接口:使用
interface
关键字定义。类使用implements
关键字实现接口。
-
-
多继承
-
抽象类:Java中类只能继承一个抽象类(单继承)。
-
接口:类可以实现多个接口(多继承)。
-
-
方法实现
-
抽象类:可以包含抽象方法(没有方法体)和具体方法(有方法体)。
-
接口:默认情况下,只包含抽象方法(没有方法体)。从Java 8开始,可以包含默认方法和静态方法。
-
-
变量
-
抽象类:可以有普通成员变量。
-
接口:变量默认是
public static final
(即常量)。
-
-
构造函数
-
抽象类:可以有构造函数,用于初始化。
-
接口:没有构造函数。
-
-
静态代码块
-
在接口中,不允许包含静态代码块。
interface MyInterface { // 静态代码块 - 这是非法的,编译时会报错 /* static { System.out.println("Static block in interface"); } */ // 静态方法 static void myStaticMethod() { System.out.println("Static method in interface"); } }
-
在抽象类中,可以包含静态代码块。
abstract class MyAbstractClass { static { System.out.println("Static block in abstract class"); } public MyAbstractClass() { System.out.println("Constructor of abstract class"); } abstract void myMethod(); } class MyClass extends MyAbstractClass { public void myMethod() { System.out.println("Method implementation in subclass"); } } public class Main { public static void main(String[] args) { MyClass obj = new MyClass(); obj.myMethod(); } }
-
抽象类是对一种事物的抽象,即对类抽象,继承抽象类的子类和抽象类本身是一种
is-a
的关系。而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。
接口是对类的某种行为的一种抽象,接口和类之间并没有很强的关联关系。
抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计。
例如,飞机和鸟是不同类的事物,但是它们都有一个共性,就是都会飞。那么在设计的时候,可以将飞机设计为一个类 Airplane,将鸟设计为一个类 Bird,但是不能将 飞行 这个特性也设计为类,因此它只是一个行为特性,并不是对一类事物的抽象描述。然后至于有不同种类的飞机,比如战斗机、民用飞机等直接继承 Airplane 即可,对于鸟也是类似的,不同种类的鸟直接继承 Bird 类即可。
继承是一个 "是不是"的关系,而 接口 实现则是 "有没有"的关系。如果一个类继承了某个抽象类,则子类必定是抽象类的种类,而接口实现则是有没有、具备不具备的关系,比如鸟是否能飞(或者是否具备飞行这个特点),能飞行则可以实现这个接口,不能飞行就不实现这个接口。
内部类
Java 内部类(Inner Class)是 Java 中一种特殊的类,它定义在另一个类的内部。
1. 成员内部类(Member Inner Class)
成员内部类定义在一个类的内部,它与外部类的实例有关。成员内部类可以访问外部类的所有成员,包括私有成员。
public class OuterClass {
private String outerField = "Outer Field";
// 成员内部类
class InnerClass {
void display() {
System.out.println("Accessing outer class field: " + outerField);
}
}
public static void main(String[] args) {
OuterClass outer = new OuterClass();
//如果想要访问内部类成员,需要先创建内部类对象
OuterClass.InnerClass inner = outer.new InnerClass();
inner.display();
}
}
2、局部内部类(Local Inner Class)
局部内部类是定义在一个方法或者一个作用域里面的类,所以局部内部类的生命周期仅限于作用域内。
局部内部类就好像一个局部变量一样,它是不能被权限修饰符修饰的,比如说 public、protected、private 和 static 等。
public class OuterClass {
void outerMethod() {
final String localVariable = "Local Variable";
// 局部内部类
class LocalInnerClass {
void display() {
System.out.println("Accessing local variable: " + localVariable);
}
}
LocalInnerClass localInner = new LocalInnerClass();
localInner.display();
}
public static void main(String[] args) {
OuterClass outer = new OuterClass();
outer.outerMethod();
}
}
3、匿名内部类(Anonymous Inner Class)
匿名内部类是 Java 中的一种特殊内部类,它没有名字,在创建实例时定义并实现其行为。
特点
-
无名字:匿名内部类在定义时没有名字。
-
即时实现:它在创建实例的同时被定义和实现。
-
局部作用域:只能在创建它的地方使用,不可以在类的其他地方引用。
-
匿名内部类是唯一一种没有构造方法的类。
-
匿名内部类可以访问定义它的外部类的所有局部变量,但这些局部变量必须是
final
或者事实上是final
的(即在初始化后不会被改变)。public class OuterClass { void outerMethod() { final String message = "Hello from anonymous inner class"; JButton button = new JButton("Click Me"); button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { System.out.println(message); // 访问外部类的 final 局部变量 } }); } }
匿名内部类就好像一个方法的参数一样,用完就没了.
语法结构:在创建匿名内部类时,需要提供一个实现(或扩展)接口(或抽象类)的代码块。
new InterfaceName() {
// 实现接口的方法
}
或者
new SuperClass() {
// 重写父类的方法
}
匿名内部类的作用主要是用来继承其他类或者实现接口,并不需要增加额外的方法,方便对继承的方法进行实现或者重写。
public class ThreadDemo {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
t.start();
}
}
4、静态内部类(Static Nested Class)
静态内部类和成员内部类类似,只是多了一个 static 关键字。
静态内部类是不允许访问外部类中非 static 的变量和方法的
public class OuterClass {
private static String outerStaticField = "Outer Static Field";
// 静态内部类
static class StaticNestedClass {
void display() {
System.out.println("Accessing outer class static field: " + outerStaticField);
}
}
public static void main(String[] args) {
OuterClass.StaticNestedClass nested = new OuterClass.StaticNestedClass();
nested.display();
}
}
封装
**封装(Encapsulation)**是一种面向对象编程的基本原则,旨在将数据(成员变量)和对数据的操作(方法)捆绑在一起,并将这些数据和操作的实现细节隐藏起来,只暴露必要的接口给外部使用。
封装的基本概念
-
数据隐藏:通过将类的字段声明为
private
,不允许外部直接访问。这些字段只能通过公开的方法(通常是public
方法)进行访问和修改。 -
接口暴露:只提供必要的接口(方法setter/getter等)给外部访问,使得类的内部实现细节对外部不可见。
封装的优势
-
提高安全性:通过限制对类内部数据的直接访问,减少了数据被外部代码意外或恶意修改的风险。
-
提高维护性:可以在不影响外部代码的情况下更改内部实现细节,从而更容易进行代码的维护和升级。
-
增强灵活性:可以通过修改 getter 和 setter 方法来控制数据的有效性和一致性,增强了对数据的控制。
-
可以提供只读方法:对于不希望修改的字段,可以只提供 getter 方法。
封装的实现
成员变量通常使用 private
关键字声明,使其只能在类内部访问。外部类通过公共的 getter 和 setter 方法访问这些变量,同时可以对数据进行验证和处理。
public class Person {
// 私有成员变量
private String name;
private int age;
// 公共构造方法
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 公共 getter 方法
public String getName() {
return name;
}
// 公共 setter 方法
public void setName(String name) {
this.name = name;
}
// 公共 getter 方法
public int getAge() {
return age;
}
// 公共 setter 方法
public void setAge(int age) {
if (age > 0) { // 验证数据有效性
this.age = age;
}
}
}
继承
**继承(Inheritance)**是一种面向对象编程(OOP)中的核心机制,它允许一个类(子类或派生类)继承另一个类(父类或基类)的属性和方法。
在 Java 语言中继承就是子类继承父类的属性和方法,使得子类对象(实例)具有父类的属性和方法,或子类从父类继承方法,使得子类具有父类相同的方法。
1、继承的基本概念
-
父类(基类):被继承的类,包含子类所需的属性和方法。
-
子类(派生类):继承父类的类,可以访问父类的公有和受保护(public 、 protected)的成员,并可以扩展或修改其功能。
子类通过
extends
关键字来继承父类。class Animal { public void sayHello()//父类的方法 { System.out.println("hello,everybody"); } } class Dog extends Animal//继承animal { } public class test { public static void main(String[] args) { Dog dog=new Dog(); dog.sayHello(); } }
-
继承关系:子类通过继承父类获得其非私有的属性和方法,同时可以定义自己的属性和方法。如代码中
dog.sayHello()
,子类对象直接调用了父类的方法。
继承访问权限符:
public
:父类的public
成员可以被子类继承并访问。
protected
:父类的protected
成员可以被子类继承并访问,但不能被其他包中的类访问。
default
(包私有):父类的默认访问权限成员只能在同一个包中访问,子类如果在不同包中就无法访问。
private
:父类的private
成员无法被子类访问。子类不能直接访问父类的私有成员。
2、单继承和多重继承
单继承:Java 支持单继承,即一个子类只能继承一个父类。这是为了避免多重继承带来的复杂性和潜在问题。
// 父类
public class Animal {
protected String name;
public void eat() {
System.out.println("This animal eats food.");
}
public void sleep() {
System.out.println("This animal sleeps.");
}
}
// 子类
public class Dog extends Animal {
public void bark() {
System.out.println("The dog barks.");
}
@Override
public void eat() {
System.out.println("The dog eats kibble.");
}
}
多继承,一个子类有多个直接的父类。这样做的好处是子类拥有所有父类的特征,子类的丰富度很高,但是缺点就是容易造成混乱。
Java 虽然不支持多继承,但是 Java 有三种实现多继承效果的方式,分别是内部类、多层继承和实现接口。
-
内部类可以继承一个与外部类无关的类,保证了内部类的独立性,正是基于这一点,可以达到多继承的效果。
-
多层继承:子类继承父类,父类如果还继承其他的类,那么这就叫多层继承。这样子类就会拥有所有被继承类的属性和方法。
-
接口:Java 通过接口支持多重继承。一个类可以实现多个接口,弥补了单继承的限制。
类和接口相比,类就是一个实体,有属性和方法,而接口更倾向于一组方法。
interface Swimmer { void swim(); } interface Runner { void run(); } class Athlete implements Swimmer, Runner { @Override public void swim() { System.out.println("Athlete swims."); } @Override public void run() { System.out.println("Athlete runs."); } } public class Main{ public static void main(String[] args){ Athlete a =new Athlete(); a.swim(); } }
继承时注意点:
父类的构造方法不能被继承
子类的构造过程必须调用其父类的构造方法.(Java 虚拟机构造子类对象前会先构造父类对象,父类对象构造完成之后再来构造子类特有的属性,这被称为内存叠加。)
如果子类的构造方法中没有显示地调用父类构造方法,则系统默认调用父类无参数的构造方法。
子类继承父类的访问修饰符作用域不能比父类小。
继承当中子类抛出的异常必须是父类抛出的异常或父类抛出异常的子异常。
class B1{ public void doA() throws Exception{} public void doB() throws Exception{} public void doC() throws IOException{} public void doD() throws IOException{} } class B2 extends B1{ //异常范围和父类可以一致 @Override public void doA() throws Exception { } //异常范围可以比父类更小 @Override public void doB() throws IOException { } //异常范围 不可以比父类范围更大 @Override public void doC() throws IOException { }//不可抛出Exception等比IOException更大的异常 @Override public void doD() throws IOException { } }
final 类不能被继承,没有类能够继承 final 类的任何特性。父类中的 final 方法可以被子类继承,但是不能被子类重写。
3、this 和 super 关键字
this 表示当前对象,是指向自己的引用。
this.属性 // 调用成员变量,要区别成员变量和局部变量
this.() // 调用本类的某个方法
this() // 表示调用本类构造方法
super 表示父类对象,是指向父类的引用。
super.属性 // 表示父类对象中的成员变量
super.方法() // 表示父类对象中定义的方法
super() // 表示调用父类构造方法
// 父类
public class Animal {
protected String name;
public void eat() {
System.out.println("This animal eats food.");
}
public void sleep() {
System.out.println("This animal sleeps.");
}
}
// 子类
public class Dog extends Animal {
public Dog() {
super(); // 调用父类的构造方法
}
public void printName() {
System.out.println(super.name); // 访问父类的 name 属性
}
@Override
public void eat() {
super.eat(); // 调用父类的 eat 方法
System.out.println("The dog eats kibble.");
}
public static void main(String[] args) {
Dog dog = new Dog();
dog.name = "Buddy";
dog.eat();
dog.printName();
}
}
4、方法重写
子类可以重写父类的方法,提供特定的实现。重写的方法在子类中具有相同的方法名、参数列表和返回类型。
类似于"外壳不变,核心内容重写"
// 父类
public class Animal {
public void eat() {
System.out.println("This animal eats food.");
}
}
// 子类
public class Cat extends Animal {
@Override
public void eat() {
System.out.println("The cat eats fish.");
}
}
// 测试类
public class Main {
public static void main(String[] args) {
Cat cat = new Cat();
cat.eat();
}
}
5**、方法重载**
方法重载与方法重写不同。重载是指在同一个类中定义多个方法名相同但参数不同的方法。重载的方法与继承关系无关。
public class Calculator {
// 重载的 add 方法
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
// 测试类
public static void main(String[] args) {
Calculator calculator = new Calculator();
System.out.println("Int add: " + calculator.add(2, 3));
System.out.println("Double add: " + calculator.add(2.5, 3.5));
}
}
6、Object 类和转型
Object(java.lang.Object)类,如果一个类没有显式声明它的父类(即没有写 extends xx),那么默认这个类的父类就是 Object 类,任何类都可以使用 Object 类的方法,创建的类也可和 Object 进行向上、向下转型。
特点:
-
Object 是类层次结构的根类,所有的类都隐式的继承自 Object 类。
-
Java 中,所有的对象都拥有 Object 的默认方法。
-
Object 类有一个构造方法,并且是无参构造方法。
-
Object 是 Java 所有类的父类,是整个类继承结构的顶端,也是最抽象的一个类。
向上转型
通过子类对象(小范围)实例化父类对象(大范围),这种属于自动转换。
父类引用变量指向子类对象后,只能使用父类已声明的方法,但方法如果被重写会执行子类的方法,如果方法未被重写那么将执行父类的方法。
向下转型
通过父类对象(大范围)实例化子类对象(小范围),在书写上父类对象需要加括号()
强制转换为子类类型。但父类引用变量实际引用必须是子类对象才能成功转型。
子类引用变量指向父类引用变量指向的对象后(一个 Son()对象),就完成向下转型,就可以调用一些子类特有而父类没有的方法 。
Object object=new Integer(666);//向上转型
Integer i=(Integer)object;//向下转型Object->Integer,object的实质还是指向Integer
String str=(String)object;//错误的向下转型,类型不匹配,虽然编译器不会报错但是运行会报错
多态
多态(Polymorphism)是面向对象编程(OOP)的核心概念之一,同一个类的对象在不同情况下表现出来的不同行为和状态。
-
子类可以继承父类的字段和方法,子类对象可以直接使用父类中的方法和字段(私有的不行)。
-
子类可以重写从父类继承来的方法,使得子类对象调用这个方法时表现出不同的行为。
-
可以将子类对象赋给父类类型的引用,这样就可以通过父类类型的引用调用子类中重写的方法,实现多态。
1、多态的基本概念
-
编译时多态(静态多态):通过方法重载实现。
编译时多态是通过方法重载实现的。方法重载是指在同一个类中定义多个方法,这些方法具有相同的名称但参数列表不同(参数的类型或数量不同)。
public class OverloadingExample { public void display(int a) { System.out.println("Argument: " + a); } public void display(String a) { System.out.println("Argument: " + a); } public void display(int a, int b) { System.out.println("Arguments: " + a + " and " + b); } public static void main(String[] args) { OverloadingExample example = new OverloadingExample(); example.display(10); example.display("Hello"); example.display(10, 20); } }
-
运行时多态(动态多态):通过方法重写和接口实现。
1、方法重写
// 父类 class Animal { public void sound() { System.out.println("Animal makes a sound"); } } // 子类 class Dog extends Animal { @Override public void sound() { System.out.println("Dog barks"); } } class Cat extends Animal { @Override public void sound() { System.out.println("Cat meows"); } } // 测试类 public class PolymorphismExample { public static void main(String[] args) { Animal myAnimal = new Animal(); // Animal 对象 Animal myDog = new Dog(); // Dog 对象 Animal myCat = new Cat(); // Cat 对象 myAnimal.sound(); myDog.sound(); myCat.sound(); } } /** Animal makes a sound Dog barks Cat meows **/
2、接口实现
// 接口 interface Animal { void sound(); } // 实现类 class Dog implements Animal { @Override public void sound() { System.out.println("Dog barks"); } } class Cat implements Animal { @Override public void sound() { System.out.println("Cat meows"); } } // 测试类 public class InterfacePolymorphismExample { public static void main(String[] args) { Animal myDog = new Dog(); Animal myCat = new Cat(); myDog.sound(); myCat.sound(); } }
2、多态与后期绑定
在运行时根据对象的类型进行后期绑定,编译器在编译阶段并不知道对象的类型,但是 Java 的方法调用机制能找到正确的方法体,然后执行,得到正确的结果。
下面例子中,在编译时,编译器只知道
myDog
和myCat
是Animal
类型的引用,但在运行时,JVM 根据对象的实际类型决定调用的方法。
myDog.makeSound()
在运行时调用Dog
类的makeSound
方法。
myCat.makeSound()
在运行时调用Cat
类的makeSound
方法。
// 父类
class Animal {
public void makeSound() {
System.out.println("Some generic animal sound");
}
}
// 子类
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Cat meows");
}
}
// 测试类
public class PolymorphismExample {
public static void main(String[] args) {
// 向上转型
Animal myAnimal = new Animal(); // 创建父类对象
Animal myDog = new Dog(); // 创建子类对象,并赋给父类引用
Animal myCat = new Cat(); // 创建另一个子类对象,并赋给父类引用
// 调用 makeSound() 方法,演示后期绑定
System.out.println("myAnimal:");
myAnimal.makeSound(); // 调用 Animal 的 makeSound 方法
System.out.println("myDog:");
myDog.makeSound(); // 调用 Dog 的 makeSound 方法
System.out.println("myCat:");
myCat.makeSound(); // 调用 Cat 的 makeSound 方法
}
}
3、多态与构造方法
构造方法中调用重写方法:在构造方法中调用重写方法需要小心,因为在父类构造方法完成之前,子类的实例部分可能尚未完全初始化。
class Animal {
public Animal() {
System.out.println("动物的构造方法被调用");
makeSound(); // 在构造方法中调用重写的方法
}
public void makeSound() {
System.out.println("某种泛泛的动物叫声");
}
}
class Dog extends Animal {
private String sound;
public Dog() {
sound = "狗叫";
System.out.println("狗的构造方法被调用");
}
@Override
public void makeSound() {
System.out.println(sound);
}
}
// 测试类
public class ConstructorExample {
public static void main(String[] args) {
Animal myDog = new Dog(); // 创建子类对象
}
}
/**
动物的构造方法被调用
null
狗的构造方法被调用
**/
在创建
Dog
对象时,首先调用Animal
的构造方法,再调用Dog
的构造方法。但是在
Animal
的构造方法中调用了makeSound()
方法。由于makeSound
被Dog
重写,因此在Dog
的sound
字段初始化之前调用,导致输出null
。
4、多态与转型
向上转型
向上转型(Upcasting)是指将子类对象赋值给父类引用。这种转换是隐式的,因为子类对象总是包含一个父类对象。
class Animal {
public void makeSound() {
System.out.println("某种泛泛的动物叫声");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("狗叫");
}
public void fetch() {
System.out.println("狗在捡东西");
}
}
public class UpcastingExample {
public static void main(String[] args) {
Animal myDog = new Dog(); // 向上转型
myDog.makeSound(); // 调用子类的 makeSound 方法
}
}
//狗叫
说明:
Animal myDog = new Dog();
通过向上转型,将Dog
对象赋给Animal
类型的引用myDog
。调用
myDog.makeSound();
实际上是调用Dog
类中的makeSound
方法,显示"狗叫"
。
向下转型
向下转型(Downcasting)是将父类引用转换为子类引用。向下转型必须显式进行,因为它可能失败。如果父类引用实际指向的不是子类对象,向下转型会导致 ClassCastException
异常。
class Animal {
public void makeSound() {
System.out.println("某种泛泛的动物叫声");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("狗叫");
}
public void fetch() {
System.out.println("狗在捡东西");
}
}
public class DowncastingExample {
public static void main(String[] args) {
Animal myAnimal = new Dog(); // 向上转型
myAnimal.makeSound(); // 调用子类的 makeSound 方法
// 向下转型
if (myAnimal instanceof Dog) {
Dog myDog = (Dog) myAnimal;
myDog.fetch(); // 调用子类特有的方法
} else {
System.out.println("转换失败,不是 Dog 类型");
}
}
}
/**
狗叫
狗在捡东西
**/
说明:
Animal myAnimal = new Dog();
创建一个Dog
对象并将其赋给Animal
类型的引用。调用
myAnimal.makeSound();
实际上是调用Dog
类中的makeSound
方法,显示"狗叫"
。通过
if (myAnimal instanceof Dog)
确保myAnimal
实际上是Dog
类型,然后进行向下转型(Dog) myAnimal
。调用myDog.fetch();
显示"狗在捡东西"
。
不安全的向下转型
class Animal {
public void makeSound() {
System.out.println("某种泛泛的动物叫声");
}
}
public class DowncastingErrorExample {
public static void main(String[] args) {
Animal myAnimal = new Animal(); // 创建一个 Animal 对象
// 不安全的向下转型,将导致 ClassCastException
try {
Dog myDog = (Dog) myAnimal;
myDog.fetch();
} catch (ClassCastException e) {
System.out.println("转换失败:不是 Dog 类型");
}
}
}
//转换失败:不是 Dog 类型
说明:
Animal myAnimal = new Animal();
创建一个Animal
对象,它不是Dog
类型。尝试将
myAnimal
向下转型为Dog
,这会导致ClassCastException
异常。使用
try-catch
语句捕获异常并输出"转换失败:不是 Dog 类型"
。