目录
Java面向对象编程(Object-Oriented Programming,OOP)是一种程序设计思想,其核心理念是将数据和操作数据的方法封装在一起,形成一个独立的单元,即“对象”。这种思想使得程序设计更加符合人类的认知方式,提高了代码的复用性、可维护性和扩展性。
面向对象编程的本质是:以类的方式组织代码,以对象的方式封装数据。
一、类与对象
1、类与对象的概念
类(Class)
类是对象的抽象表示,是创建对象的模板,它定义了对象的属性(字段)和方法,以及这些属性和方法之间的关系。类由类名、成员变量(属性)、成员方法(行为)等组成。
对象(Object)
对象是类的实例,每个对象都有自己的状态和行为,状态通过成员变量表示,行为通过方法表示。对象通过调用其所属类的方法来执行操作,并通过访问其属性来获取或修改状态。
2、类与对象的使用
定义类
在Java中,使用class
关键字来定义类。
public class Student {
// 属性
String name;
int age;
// 方法
public void study() {
System.out.println(this.name + "在学习");
}
}
我们定义了一个最简单的类,里面有属性和方法。
创建对象
在Java中,使用new
关键字来创建对象。
public class Application {
//一个项目应该只存在一个main方法
public static void main(String[] args) {
// 类是抽象的,需要实例化,实例化会返回一个对象
Student s1 = new Student();
s1.name = "张三"; // 如果不赋值默认为null
s1.age = 13; // 如果不赋值默认为0
// 访问属性
System.out.println(s1.name); //输出:张三
System.out.println(s1.age); //输出:13
// 调用方法
s1.study(); //输出:张三在学习
}
}
二、构造器
在Java中,构造器(Constructor)是一种特殊的方法,用于在创建对象时初始化对象的状态。
1、构造器的概念
-
名称:构造器的名称必须与类名完全相同。
-
返回类型:构造器没有返回类型,甚至连
void
也没有。 -
访问修饰符:可以是
public
、protected
、private
或默认。 -
参数:可以有参数,也可以没有参数,没有参数的构造器称为无参构造器,有参数的称为有参构造器。
-
注意:定义了有参构造器之后,如果想使用无参构造,就必须显式地定义一个无参的构造器。
2、构造器的作用
-
为新创建的对象分配内存空间,并初始化对象的成员变量。
-
可以在构造器中加入逻辑,确保对象在创建时处于有效状态。
-
使用new关键字,本质是在调用构造器。当使用
new
关键字创建对象时,构造器会被隐式调用,负责初始化对象的状态。
3、默认构造器
如果一个类中没有定义任何构造器,编译器会自动提供一个无参的默认构造器,一旦定义了构造器(无论是有参还是无参),编译器将不再提供默认构造器。
4、自定义构造器
无参构造器
我们也可以在类中显式定义无参构造器,不用传任何参数。
public class Person {
String name;
public Person() {
this.name = "张三";
}
}
有参构造器
有参构造器需要在类中显式定义,并且可以接受一个或多个参数,用于初始化对象的属性。
public class Person {
String name;
public Person(String name) {
this.name = name;
}
}
5、重载构造器
一个类可以有多个构造器,只要它们的参数列表不同(方法重载)。
public class Person {
String name;
public Person() {
this.name = "张三";
}
public Person(String name) {
this.name = name;
}
}
6、this关键字在构造器中的使用
(1)引用当前对象的实例变量
当构造器的参数与类的成员变量同名时,为了区分它们,我们需要使用this
关键字来引用当前对象的实例变量,this
在这里起到了一个“指向当前对象”的作用,确保我们能够正确地访问和修改对象的成员变量。
public class Example {
private int number;
public Example(int number) {
this.number = number; // 使用this引用当前对象的number成员变量
}
}
在本例中,构造器参数number
与成员变量number
同名。通过this.number = number;
,我们将构造器参数的值赋给了当前对象的成员变量。
(2)调用当前类的其他构造器
一个构造器可以通过使用this()
语法来调用同一个类的另一个构造器,这种机制被称为构造器链(Constructor Chaining),它允许我们重用构造器代码,避免重复。
public class Rectangle {
private int width;
private int height;
// 无参构造器
public Rectangle() {
this(0, 0); // 调用带参构造器,初始化width和height为0
}
// 带参构造器
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
}
在本例中,无参构造器通过this(0, 0);
调用了带参构造器,从而避免了重复编写初始化width
和height
的代码。
三、封装(Encapsulation)
封装涉及到将对象的属性(字段)和行为(方法)结合在一起,并隐藏对象的内部实现细节,仅对外暴露有限的接口供外部访问。封装的主要目的是提高软件组件的模块化程度,增强代码的可维护性和安全性。
1、封装的好处
-
数据隐藏:通过封装可以将对象的内部状态(属性)隐藏起来,只暴露必要的接口(方法)供外部访问。这有助于保护对象的状态不被随意修改,从而维护对象的完整性。
-
模块化:封装使得每个类都成为一个独立的模块,只负责特定的功能。这有助于降低代码的耦合度,提高代码的可重用性和可维护性。
-
安全性:通过封装可以限制对对象属性的直接访问,从而防止意外修改或破坏对象的状态。此外,还可以添加访问控制逻辑(如检查输入值的合法性)来进一步保护对象。
-
灵活性:由于封装隐藏了内部实现细节,因此可以在不改变外部接口的情况下修改类的内部实现。这提供了很大的灵活性,使得代码更容易适应变化。
-
访问控制:通过提供公共的get和set方法,封装允许对数据进行可控访问。这些方法可以包含数据验证逻辑,确保数据的完整性和合理性。
2、封装的用法
定义类
首先,定义一个类来表示一个对象,类中包含对象的属性和方法。
public class Student {
// 属性私有
private String name;
// 提供一些可以操作这些属性的方法
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
使用类
通过创建类的实例来使用封装好的对象。
public class Application {
public static void main(String[] args) {
// 实例化一个对象
Student s1 = new Student();
// 使用暴露出来的方法
s1.setName("张三");
System.out.println(s1.getName());
}
}
四、继承(Inheritance)
继承允许一个类(称为子类或派生类)继承另一个类(称为父类或基类)的属性和方法,通过关键字extends
来实现。通过继承,子类可以复用父类的代码,并在此基础上添加新的属性和方法,或者修改父类的方法实现特定的需求。
1、继承的特点
-
单继承:Java只支持单继承,即一个类只能有一个直接父类。但Java通过接口实现了多重继承的效果,即一个类可以实现多个接口。
-
传递性:继承关系是传递的。如果类C继承类B,类B继承类A,则类C既有从类B那里继承下来的属性与方法,也有从类A那里继承下来的属性与方法。
-
方法重写:子类可以重写父类的方法,又叫做方法覆盖(Override),以提供不同的实现。重写方法时,子类的方法签名必须与父类的方法签名相同。
-
构造方法:子类不能直接继承父类的构造方法,但可以通过调用
super()
来调用父类的构造方法。 -
访问控制:子类可以访问父类的公共(public)、受保护(protected)和默认(默认)访问级别的成员,但不能访问私有(private)成员。
2、继承的实现
父类定义
父类(或基类)是包含通用属性和方法的类。
public class Person {
public void sayHello() {
System.out.println("Hello");
}
}
定义一个Person
类作为父类,包含一些基本的属性和方法,如sayHello()
方法。
子类定义
子类(或派生类)是继承父类属性和方法的类,并可以添加自己的属性和方法。使用extends
关键字声明一个类继承另一个类。
public class Student extends Person {
public void sayHello() {
System.out.println("Hello,I'm student");
}
}
在本例中,Student
类通过extends
关键字继承了Person
类,重写sayHello()
方法以提供特定的实现。
创建子类对象并访问父类的成员
通过子类的构造函数创建子类对象,在创建子类对象时,会隐式地调用父类的构造函数(如果子类没有显式地调用父类的其他构造函数)。
子类对象可以访问父类的非私有成员(属性和方法)以及自己的成员,如果子类重写了父类的方法,则通过子类对象调用该方法时会执行子类的实现。
public class Demo {
public static void main(String[] args) {
Student student = new Student();
student.sayHello();
}
}
super关键字的简单使用
在子类中,可以使用super
关键字来引用父类的构造函数、属性和方法。
例如,在子类的构造函数中使用super()
来调用父类的构造函数。
public class Student extends Person {
public Student() {
super(); // 调用父类的构造函数
}
public void sayHello() {
super.sayHello();
System.out.println("Hello,I'm student");
}
}
在本例中,super.sayHello()
的调用是可选的,这里只是为了演示如何使用super
关键字。实际上,如果子类完全重写了父类的方法,通常不需要在子类的方法中调用父类的实现。
3、继承的优缺点
优点
-
实现了数据和方法的共享,提高了代码的复用性。
-
提高了代码的可扩展性,通过继承可以方便地扩展新功能。
-
对父类代码的修改会反映到所有子类中,提高了代码的可维护性。
缺点
-
如果过度使用继承,可能会导致类层次结构过于复杂,难以维护。
-
增强了类的耦合性,当父类发生变化时,子类实现也不得不跟着变化,削弱了子类的独立性。
4、super关键字
Java中的super
关键字是一个非常重要的概念,主要用于在子类中访问父类的成员变量、方法和构造函数。
1.引用父类对象
super
可以被理解为指向父类对象的一个指针,它代表当前对象的父类型特征。通过super
,子类可以访问父类的属性和方法,从而实现继承关系中的功能共享。
定义父类
public class Person {
protected String name = "李四";
public void print() {
System.out.println("Person");
}
}
定义子类
public class Student extends Person {
private String name = "王五";
public void print() {
System.out.println("Student");
}
public void test(String name) {
System.out.println(name);
System.out.println(super.name);
System.out.println(this.name);
}
}
调用父类对象
public class Application {
public static void main(String[] args) {
Student student = new Student();
student.test("张三"); // 输出:张三 李四 王五
}
}
2.调用父类构造方法
在子类的构造方法中,如果需要调用父类的构造方法,可以使用super()
来显式调用。如果没有显式调用,Java会默认调用父类的无参构造方法。
super()
调用父类的构造方法,必须位于构造方法的第一个;super()
只能出现在子类的方法或者构造方法中;super()
和this
不能同时调用构造方法。
定义父类
public class Person {
public Person() {
System.out.print("Person" + " ");
}
}
定义子类
public class Student extends Person {
public Student() {
System.out.println("Student");
}
}
调用父类构造方法
public class Application {
public static void main(String[] args) {
Student student = new Student();
// 输出:Person Student
}
}
5、方法重写
方法重写(Method Overriding)允许子类重新定义父类中已有的方法,以便子类可以根据自己的需要修改方法的实现细节。通过方法重写,可以实现运行时多态性,使得程序更加灵活和可扩展。
注意:
-
子类重写的方法必须与父类的方法具有相同的参数列表,包括参数的数量、类型和顺序。
-
子类重写的方法的返回类型必须与父类的方法相同,或者可以是父类返回类型的子类型(协变返回类型)。
-
子类重写的方法的访问修饰符不能比父类的方法更严格,可以更宽松。
-
子类重写的方法可以减少或消除父类方法抛出的已检查异常,但不能抛出新的或更广泛的已检查异常。
-
构造方法不能被重写;final方法不能被重写;静态方法不能被重写,但可以被隐藏(方法隐藏)。
示例:
// 父类
class Person {
void sayHello() {
System.out.println("Hello,I am a person.");
}
}
// 子类
class Student extends Person {
@Override
void sayHello() {
System.out.println("Hello,I am a student.");
}
}
public class Application {
public static void main(String[] args) {
Person person = new Person();
Student student = new Student();
person.sayHello(); // 输出:Hello,I am a person.
student.sayHello(); // 输出:Hello,I am a student.
}
}
在本例中,Student
类继承了Person
类,并重写了sayHello
方法。当通过Personl
类型的引用调用sayHello
方法时,实际执行的是子类Student
中重写的方法。
五、多态(Polymorphism)
多态是指同一个行为在不同对象中表现出不同的形态,产生不同的结果。当通过父类引用调用这个方法时,实际执行的是子类重写的方法,这就是多态的体现。多态主要通过继承、接口和方法重写来实现,并且分为编译时多态(静态多态)和运行时多态(动态多态)两种形式。
1、多态的实现条件
要实现多态,需要满足以下三个条件:
-
继承关系:子类必须继承父类。
-
方法重写:子类需要重写父类的方法。
-
父类引用指向子类对象:通过父类类型的引用指向子类对象。
2、多态的类型
-
编译时多态(静态多态) :通过方法重载实现。方法重载是在同一个类中定义多个同名方法,但参数列表不同,编译器在编译时就能确定调用哪个方法。
-
运行时多态(动态多态) :通过继承和接口实现。运行时多态是在运行期间根据对象的实际类型动态决定调用哪个方法,这是Java中多态的主要形式。
3、实现多态的方式
1. 方法重载(Method Overloading)
方法重载是指在同一个类中,允许存在一个以上的同名方法,只要它们的参数个数或者参数类型不同即可。编译器根据方法的参数列表区分不同的方法。
class Example {
void display(int a) {
System.out.println("Integer: " + a);
}
void display(String a) {
System.out.println("String: " + a);
}
}
public class Main {
public static void main(String[] args) {
Example obj = new Example();
obj.display(10); // 调用 display(int a)
obj.display("Hello"); // 调用 display(String a)
}
}
2. 方法重写(Method Overriding)
方法重写是指子类重新定义父类中已经定义过的方法。重写的方法必须具有相同的名称、返回类型和参数列表。重写的方法允许子类根据自身的需求对父类中的方法进行修改。
class Person {
void sayHello() {
System.out.println("Hello");
}
}
class Student extends Person {
@Override
void sayHello() {
System.out.println("Hello,I am a student.");
}
void study(){
System.out.println("I like study.");
}
}
public class Demo {
public static void main(String[] args) {
Person s1 = new Student();
Student s2 = new Student();
s1.sayHello(); // 输出:Hello,I am a student.
s2.sayHello(); // 输出:Hello,I am a student.
((Student) s1).study(); // 输出:I like study.
s2.study(); // 输出:I like study.
}
}
在本例中,s1
是一个 Person
类型的引用,但它指向的是一个 Student
对象。当调用 sayHello()
方法时,JVM 会根据对象的实际类型来决定调用哪个方法,这就是运行时多态。
4、多态的优点
-
提高代码的可重用性:通过继承和接口,可以编写通用的代码来处理多种类型的数据。
-
增强程序的灵活性和扩展性:多态使得程序能够适应变化,更容易添加新的功能或修改现有功能。
-
简化开发过程:开发者可以通过多态特性编写更加简洁和高效的代码。
5、注意事项
-
访问权限:子类重写的方法不能拥有比父类方法更严格的访问权限。例如,如果父类的方法是
public
,那么子类的方法也必须是public
。 -
异常:子类重写的方法可以抛出比父类方法更少或更具体的异常,但不能抛出新的或更一般的异常。
-
返回类型:在 Java 5 及其以后的版本中,如果父类方法是返回类型的协变(即返回类型是某个类的引用),那么子类重写的方法可以返回该类的子类。
6、instanceof关键字
在Java中,instanceof
关键字是一个二元运算符,用于检查对象是否是特定类的实例 。它是Java反射机制的一部分,用于在运行时检查对象的类型。这在处理多态性时尤其有用,可以确定一个对象是否是某个特定类或其子类的实例。
1.基本使用
instanceof 主要用于判断对象是否属于某个类或接口的实例。
public class Demo {
public static void main(String[] args) {
String str = "Hello";
System.out.println(str instanceof String); // 输出 true
}
}
2.继承关系中的使用
当对象是某个类的子类时,instanceof
可以用来判断该对象是否属于父类或其子类。
public class Demo {
public static void main(String[] args) {
class A {
}
class B extends A {
}
A a = new B();
System.out.println(a instanceof B); // 输出 true
System.out.println(a instanceof A); // 输出 true
}
}
3.接口实现
如果对象实现了某个接口,instanceof
可以用来检查该对象是否实现了指定的接口。
public class Demo {
public static void main(String[] args) {
interface MyInterface {
}
class MyClass implements MyInterface {
}
MyClass obj = new MyClass();
System.out.println(obj instanceof MyInterface); // 输出 true
}
}
4.避免类型转换异常
使用 instanceof
可以避免在向下转型时抛出 ClassCastException
。
public class Demo {
public static void main(String[] args) {
Object obj = "Hello";
if (obj instanceof String) {
String str = (String) obj;
System.out.println(str.length()); // 输出 5
}
}
}
5.处理 null
值
当对象为 null
时,instanceof
的结果总是 false
。
public class Demo {
public static void main(String[] args) {
String str = null;
System.out.println(str instanceof String); // 输出 false
}
}
6.注意事项
-
instanceof
运算符只能用于对象引用变量,不能用于基本数据类型。 -
如果左侧的对象是
null
,则instanceof
的结果总是false
。 -
在多态编程中,
instanceof
是实现运行时类型检查的重要工具,有助于提高代码的安全性和灵活性。
7、类型转换
类型转换在多态中扮演着重要角色,它分为向上转型(自动类型转换)和向下转型(强制类型转换)两种。
1.向上转型(自动类型转换)
向上转型是指将子类对象赋值给父类引用变量。这种转换是自动的,不需要显式声明类型转换符。向上转型的主要目的是利用多态性,使得父类引用可以指向子类对象,从而调用子类重写的方法。
语法格式:父类类型 父类引用变量 = new 子类类型();
class Person {
public void eat() {
System.out.println("人在吃东西");
}
}
class Student extends Person {
public void study() {
System.out.println("学生在学习");
}
}
public class Test {
public static void main(String[] args) {
Student stu = new Student();
Person per = stu; // 向上转型,自动完成
per.eat(); // 调用父类方法
}
}
在本例中,Student
类的对象stu
被赋值给了Person
类型的引用per
,这就是向上转型。通过向上转型,我们可以用父类的引用来统一管理不同子类的对象,提高了代码的通用性和灵活性。
2.向下转型(强制类型转换)
向下转型是指将父类引用转换为子类引用。这种转换需要显式声明类型转换符,并且必须确保父类引用指向的对象实际上是子类对象,否则会导致ClassCastException
异常。
语法格式:子类类型 子类引用变量 = (子类类型) 父类引用变量;
class Person {
public void eat() {
System.out.println("人在吃东西");
}
}
class Student extends Person {
public void study() {
System.out.println("学生在学习");
}
}
public class Test {
public static void main(String[] args) {
Student stu = new Student();
Person per = stu; // 向上转型
// 向下转型,需要强制转换
Student s = (Student) per;
s.study(); // 调用子类方法
}
}
在本例中,Person
类型的引用per
被强制转换为了Student
类型的引用s
,这就是向下转型。通过向下转型,我们可以访问子类特有的方法和属性。但是,向下转型是有风险的,如果父类引用实际上指向的不是子类对象,强制转换就会导致ClassCastException
。
3.注意事项
-
继承关系:只有在存在继承关系的基础上,才可以进行类型转换。
-
类型判断:在进行向下转型之前,最好使用
instanceof
关键字来判断父类引用是否指向了目标子类类型的对象。这可以避免ClassCastException
的发生。 -
避免过度使用:虽然向下转型可以让我们访问子类特有的功能和属性,但是过度使用会导致代码的可读性和可维护性降低。因此,应该尽量避免不必要的向下转型。