一、面向对象与面向过程
1.什么是面向过程?
面向过程就是分析一个问题所需要的解决步骤,然后用函数把这些步骤一步一步的给实现,使用的时候一个一个的依次调用即可。
比如:洗衣服
按照面向的过程思想:
第一步:我把洗衣机门打开
第二步:把衣服放入洗衣机里
第三步:我倒入洗衣液
第四步:我把洗衣机门关上
这就能明显看出来,这个面向过程就是一步一步的实现需要做的事
用代码表示
public void openDoor(){} //开门
public void putIn(){} //放入衣服
public void putOn(){} //倒入洗衣液
public void closeDoor(){} //关门
依次调用上面方法即可
2.什么是面向对象?
对象,就是对问题中的事物的抽象
面向对象:就是把现实中的事物都抽象化为“对象”。每个对象是唯一的,并且都可以拥有自己的属性和行为。我们就可以通过调用这些对象的方法、属性去解决问题
简单来说
面向对象
- 面向: 拿、找
- 对象: 能干活的东西
- 面向对象编程(OOP): 拿东西过来做对应的事
用面向对象思想解决洗衣服
例如在这个事件中:
衣服作为一个对象;
洗衣机作为一个对象;
洗衣机有这些功能:开门、装衣服、装洗衣液、关门
class laundryMachine{
public void open(衣服){} //开门
public void putIn(衣服){} //放进衣服
public void putOn(洗衣液){} //放进洗衣液
public void close(衣服{} //关门
}
可以看出每个对象是独立的,有属于它自己的功能,只需要专心实现自己的功能就好。所以在建立对象模型阶段,仅仅关注对象有什么的功能,但不考虑如何实现这些功能。
面向对象的好处:面向对象就是把现实问题抽象为对象,通过调用每个对象的属性或功能去解决问题。
二、类和对象的关系
1、基本概念
对象:对象是由数据(描述事物的属性)和作用于数据的操作(体现事物的行为)组成的封装体,描述客观事物的一个实体,是构成系统的基本单元。
类:类是对一组有相同数据和相同操作的对象的定义,是对象的模板,其包含的方法和数据描述一组对象的共同行为和属性。类是在对象之上的抽象,对象则是类的具体化,是类的实例。类可有其子类,也可有其他类,形成类层次结构。
简单来说
- 类:是对象共同特征的描述
- 对象: 是真实存在的具体东西
在Java中,必须先设计类,才能获得对象
2、类与对象的区别
1)类是对象的抽象,而对象是类的具体实例。类是抽象的,不占用内存,而对象是具体的,占用存储空间。
2)类是一组具有相同属性和行为的对象的抽象。我们可以将类看做是创建对象蓝图,对象根据这个蓝图去具体实现某个东西。
比如:给一个“表”的蓝图,设计出了机械表,电子表等,你会发现,这些表都具有相同的行为--看时间
所以,类的实例化结果就是对象,而对一类对象的抽象就是类,类描述了一组有相同属性和相同方法的对象。
三、基本特征:封装、继承、多态
1、封装
(一)基本概念
封装是将数据和代码捆绑到一起,形成一个独立的单元(即对象)。对象的某些数据和代码可以是私有的,不能被外界直接访问,以此实现对数据和代码的保护和隐藏。封装提高了系统的安全性和可维护性。
(二)四种访问控制级别
- public:对外公开,访问级别最高
- protected:只对同一个包中的类或者子类公开
- 默认:只对同一个包中的类公开
- private:不对外公开,只能在对象内部访问,访问级别最低
封装的实现
-
修改属性的可见性:使用访问修饰符(如
private
、protected
、public
等)来控制类成员的可见性。通常,将类的属性(即数据成员)设置为private
,以限制外部直接访问。 -
提供公共的getter和setter方法:为类的私有属性提供公共的getter和setter方法,以允许外部通过这些方法访问和修改属性的值。getter方法用于返回属性的值,setter方法用于设置属性的值。
-
在setter方法中加入控制逻辑:在setter方法中,可以加入控制逻辑来限制对属性的不合理访问和修改。例如,可以对输入值进行合法性检查,如果不合法则拒绝赋值。
封装的优点
-
良好的封装能够减少耦合,符合程序设计追求“高内聚,低耦合”。
-
类内部的结构可以自由修改。
-
可以对成员变量进行更精确的控制。
-
隐藏信息实现细节。
2、继承
继承是一种层次模型,允许子类继承父类的属性和方法。通过继承,子类可以重用父类的代码,减少代码的冗余,提高代码的重用性。同时,子类还可以添加或覆盖父类的方法,实现功能的扩展和定制。
(一)语法
class A extends B{}
一共继承了哪些东西呢?这要分两种情况
- 当A和B在同一包下,A继承了B中public、protected和默认访问级别的成员变量和成员方法
- 当A和B不在同一包下,A继承了B中public、protected成员变量和成员方法
重写
重写是子类对父类中已有方法的一种重新实现。当子类继承自父类时,子类可以定义一个与父类中签名完全相同(方法名、返回类型、参数列表都相同)的方法,但实现(即方法体)可能与父类中的方法不同。这样,当通过子类的实例调用该方法时,将执行子类中的方法实现,而不是父类中的。
重写规则:
- 方法名、参数列表必须相同。
- 返回类型必须与被重写方法的返回类型相同或是其子类型(Java 5及以后版本支持协变返回类型)。
- 访问权限不能比被重写的方法的访问权限更低(但可以更高)。
- 被重写的方法不能是
final
的,否则该方法不能被重写。 - 被重写的方法不能是
static
的,因为静态方法是属于类的,不是属于实例的,因此不存在多态的概念。 - 子类中的重写方法可以抛出新的检查型异常(checked exceptions),或者更不具体的异常,但不能抛出新的运行时异常(runtime exceptions),或者比被重写方法声明的更具体的检查型异常。
假设我们有一个父类Animal
和一个子类Dog
,Animal
类中有一个方法makeSound()
用于发出声音,而Dog
类重写了这个方法以发出狗的叫声。
// 父类
class Animal {
// 父类中的方法
public void makeSound() {
System.out.println("Some generic sound");
}
}
// 子类
class Dog extends Animal {
// 子类重写父类中的方法
@Override // @Override注解是可选的,但建议加上,用于编译时检查
public void makeSound() {
System.out.println("Woof");
}
}
public class TestOverriding {
public static void main(String[] args) {
Animal myAnimal = new Animal();
Animal myDog = new Dog(); // 父类引用指向子类对象,这是多态的一种体现
myAnimal.makeSound(); // 输出: Some generic sound
myDog.makeSound(); // 输出: Woof,因为myDog实际上是Dog的实例,调用了Dog类中的方法
}
}
在这个例子中,Dog
类重写了Animal
类中的makeSound()
方法。当通过Dog
的实例调用makeSound()
时,会执行Dog
类中的方法实现,即发出“Woof”的声音。
重载
重载是在同一个类中,允许存在多个同名方法,只要它们的参数列表不同即可。参数列表的不同可以体现在参数的类型、参数的个数或参数的顺序上。重载与方法的返回类型无关,仅根据方法名和参数列表来区分。
重载规则:
- 方法名必须相同。
- 方法的参数类型、个数、顺序至少有一项不同
- 方法的返回类型可以相同也可以不同,但仅根据返回类型来区分重载是不允许的。
- 访问修饰符、异常声明和是否为
static
都可以不同,但这些都不是重载的考虑因素。
接下来,我们在同一个类中展示方法重载。假设我们有一个Calculator
类,它提供了几个重载的add()
方法,用于执行不同类型的加法操作。
class Calculator {
// 第一个add方法,用于两个整数的加法
public int add(int a, int b) {
return a + b;
}
// 第二个add方法,用于两个浮点数的加法(这是方法重载,因为参数列表不同)
public double add(double a, double b) {
return a + b;
}
// 第三个add方法,用于三个整数的加法(这也是方法重载)
public int add(int a, int b, int c) {
return a + b + c;
}
}
public class TestOverloading {
public static void main(String[] args) {
Calculator calc = new Calculator();
System.out.println(calc.add(1, 2)); // 调用第一个add方法,输出: 3
System.out.println(calc.add(1.5, 2.5)); // 调用第二个add方法,输出: 4.0
System.out.println(calc.add(1, 2, 3)); // 调用第三个add方法,输出: 6
}
}
在这个例子中,Calculator
类中有三个add()
方法,它们的方法名相同但参数列表不同。这构成了方法重载。在main
方法中,我们通过传递不同数量和类型的参数来调用不同的add()
方法。
3、多态
多态是指不同对象对同一消息作出不同响应的能力。在面向对象中,多态通常通过接口或抽象类来实现。多态使得程序具有更好的灵活性和可扩展性,能够在运行时根据对象的实际类型来调用相应的方法。
简单来说
多态:对于同一个行为,不同的子类对象具有不同的表现形式。
多态存在的3个条件:
1)继承
2)重写
3)父类引用指向子类对象。
以下是一个简单的Java示例,展示了多态性的应用:
class Animal {
public void sound() {
System.out.println("动物发出声音");
}
}
class Dog extends Animal {
@Override
public void sound() {
System.out.println("狗发出汪汪声");
}
}
class Cat extends Animal {
@Override
public void sound() {
System.out.println("猫发出喵喵声");
}
}
public class Main {
public static void main(String[] args) {
Animal animal1 = new Dog(); // 父类引用指向子类Dog对象
Animal animal2 = new Cat(); // 父类引用指向子类Cat对象
animal1.sound(); // 输出: 狗发出汪汪声
animal2.sound(); // 输出: 猫发出喵喵声
}
}
在这个示例中,Animal
类是父类,Dog
和Cat
类是它的子类。通过将父类的引用变量animal1
和animal2
分别指向Dog
和Cat
对象,实现了多态性。在运行时,根据引用变量的实际类型(即对象的实际类型),调用了相应子类的方法,从而输出了不同的声音。
多态的优缺点
优点
- 增强代码复用性:多态允许子类继承父类的属性和方法,同时还可以添加自己特有的属性和方法或者覆盖父类的方法,从而实现代码的重用。
- 易于维护和扩展:由于多态的存在,我们可以对现有的类进行扩展,而无需修改现有代码。这使得软件系统更加模块化,便于管理和升级。
- 提高灵活性:多态允许我们在运行时根据实际对象的类型来决定调用哪个方法,这为设计提供了极大的灵活性。
- 降低耦合度:多态有助于降低类和类之间的依赖关系,因为可以通过抽象的方式处理各种具体的对象,而不是直接与具体类交互。
缺点
- 性能开销:多态性的实现通常涉及动态绑定,即在运行时确定要调用的方法版本。这个过程比静态绑定更耗时,可能会带来一定的性能开销。然而,对于现代的JIT编译器来说,这种开销通常非常小。
- 复杂性增加:多态性引入了更多的抽象层次,这可能使代码逻辑变得更加复杂,对于初学者来说可能难以理解。
- 过度使用可能导致设计混乱:如果过多地使用多态,可能会导致设计变得过于复杂,难以跟踪和维护。
如何理解“父身子象”这个词?
"父身子象"这个词并不是一个标准的术语,但根据它的字面意思和Java的面向对象编程(OOP)特性,我们可以将其理解为一种对继承关系及其结果的形象描述。不过,为了更准确地解释这个概念,我们通常会使用“父类(Parent Class)”和“子类(Subclass)”或“派生类(Derived Class)”这样的术语,以及它们之间的“对象(Object)”来阐述。
在Java中,继承是面向对象编程的一个核心概念,它允许我们定义一个类(称为子类或派生类)来继承另一个类(称为父类或基类)的属性和方法。这里,“父”指的是被继承的类,“子”指的是继承自父类的类,“身”可以理解为继承关系的本质,即子类拥有父类的所有非私有(non-private)属性和方法,而“象”则是指这种继承关系在Java中实例化后的对象所展现出来的形态或特征。
简单来说,当我们说“父身子象”时,可以理解为:
- 父:指的是一个基类或父类,它定义了一组基本的属性和方法。
- 子:指的是一个从父类继承而来的子类或派生类,它可以添加新的属性和方法,也可以重写(Override)父类中的方法。
- 身:代表继承关系的本质,即子类“继承”了父类的所有非私有成员。
- 象:是这种继承关系在Java代码中实例化后所呈现出的对象,这些对象既包含了父类的特性,也可能包含了子类特有的特性。
然而,为了更准确地表达这一概念,我们通常会说“父类与子类之间的关系”,或者“通过继承,子类获得了父类的属性和方法”。
在Java中,使用extends
关键字来实现类的继承,例如:
class Animal {
// Animal类的定义
}
class Dog extends Animal {
// Dog类继承自Animal类
}
在这个例子中,Animal
是Dog
的父类(或基类),而Dog
是Animal
的子类(或派生类)。通过继承,Dog
类的对象将拥有Animal
类定义的所有非私有属性和方法(如果Animal
类中有的话),同时Dog
类还可以定义自己的属性和方法。
在多态的情况下,若想恢复子类的身份,该进行哪两步操作?
在多态的情况下,如果你有一个父类类型的引用指向了一个子类对象,并且你想要在运行时恢复或识别出这个对象的确切子类身份(即“恢复子类的身份”),你通常会通过两个步骤来实现:
1.类型判断:
这一步是判断对象是否属于某个特定的子类类型。在Java中,这通常通过使用instanceof
操作符来完成。instanceof
操作符用于在运行时检查对象是否是特定类的一个实例,或者是否实现了特定的接口。
Animal animal = new Dog();
if (animal instance of Dog) {
}
2.类型转换:
一旦你确定了对象确实是某个特定子类的实例,你就可以将该对象从父类类型转换为子类类型。在Java中,这通常通过显式类型转换(也称为向下转型)来完成。但是,进行这种转换之前,你应该确保对象实际上是那个子类的实例,否则将会抛出ClassCastException
。
if (animal instance of Dog) {
Dog dog = (Dog) animal;
// 现在你可以使用 Dog 特有的方法和属性了
}
这两个步骤是在多态中恢复子类身份的标准做法。它们允许你在运行时根据对象的实际类型来执行不同的操作,这是多态性的一个重要应用。然而,需要注意的是,过度依赖类型判断和类型转换可能会破坏代码的清晰性和可维护性,因此在设计时应尽量避免不必要的类型判断和转换。如果可能的话,使用设计模式(如访问者模式、策略模式等)来避免直接的类型判断和转换,可以使代码更加灵活和可扩展。
四、this和super
- this:代表当前对象的引用,用于访问当前对象的成员变量和方法,调用当前对象的另一个构造方法,以及作为方法的返回值。在静态方法或静态上下文中不可用。
- super:用于指代父类对象的一个特殊引用,用于访问父类的成员变量和方法,以及调用父类的构造方法。在静态方法或静态上下文中同样不可用。
this和super的区别
a、代表的对象不同
- this:代表当前对象的引用,即谁来调用这个方法,
this
就代表谁。在类的内部,this
可以指向当前对象的属性和方法。 - super:代表当前对象的父类引用。在子类中,
super
可以用来访问父类的属性和方法。
b、使用前提不同
- this:没有继承关系也可以使用
this
。它主要用于当前类的内部,指向当前对象的实例。 - super:只能在有继承关系的类中使用
super
。在子类中,通过super
可以访问父类的成员(包括属性和方法)。
c、调用的构造函数不同
- this:在构造方法中,
this()
用于调用本类的其他构造方法。需要注意的是,this()
调用必须放在构造方法的第一行,且一个构造方法中只能调用一次其他构造方法。 - super:在子类的构造方法中,
super()
用于调用父类的构造方法。同样地,super()
调用也必须放在构造方法的第一行。如果子类构造方法中没有显式调用父类的构造方法,则会默认调用父类的无参构造方法(如果父类没有定义无参构造方法,则编译时会报错)。
d、访问成员变量和方法的不同
- this:在类的非静态成员方法中,
this
用于访问当前对象的成员变量和方法。如果当前类中没有找到相应的成员变量或方法,则会继续在父类中查找(但这不是this
的直接作用,而是继承机制的结果)。 - super:在子类的非静态成员方法中,
super
用于访问父类的成员变量和方法。如果子类重写了父类的方法,那么通过super
可以调用到父类被重写的方法。
this举例
public class Person {
private String name;
// 使用this引用成员变量
public void setName(String name) {
this.name = name; // 这里的this.name指的是Person类的成员变量name
}
// 构造方法中使用this调用另一个构造方法
public Person() {
this("Unknown"); // 调用带有String参数的构造方法
}
public Person(String name) {
this.name = name; // 初始化成员变量
}
// 方法中使用this调用本类的方法
public void introduce() {
System.out.println("Hello, my name is " + this.getName());
}
// 辅助方法,用于演示this在方法中的使用
public String getName() {
return name;
}
}
注意事项:
- 构造方法中的this调用:
this
调用另一个构造方法时,必须作为构造方法的第一条语句。 - 使用场景:
this
可以在构造方法、实例方法中使用,但不能在静态方法或静态块中使用,因为静态成员属于类本身,不依赖于对象实例。 - 避免混淆:当局部变量与成员变量同名时,使用
this
明确指定成员变量。
super举例
class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void eat() {
System.out.println(name + " is eating.");
}
}
class Dog extends Animal {
private String breed;
// 使用super调用父类构造方法
public Dog(String name, String breed) {
super(name); // 调用Animal类的构造方法
this.breed = breed;
}
// 重写父类方法,并使用super调用父类方法
@Override
public void eat() {
super.eat(); // 调用Animal类的eat方法
System.out.println(name + " is eating bones.");
}
}
注意事项:
- 构造方法中的super调用:
super
调用父类构造方法时,也必须是子类构造方法的第一条语句。如果子类构造方法中没有显式调用父类构造方法,则会默认调用父类的无参构造方法(如果父类中存在无参构造方法)。 - 使用场景:
super
可以在子类的构造方法、实例方法中使用,但不能在静态方法或静态块中使用。 - 访问权限:
super
用于访问父类的成员(包括变量和方法),但这些成员必须具有可访问性(即不是私有的,或者子类在相同包内等)。 - 避免重复调用:在构造方法中,
this
和super
的调用只能出现其一,且必须作为第一条语句。这是因为它们都涉及到构造方法的调用,而Java不允许多重继承,因此不允许在同一个构造方法中同时调用多个构造方法。
五、抽象类
普通类是一个完善的功能类,可以直接产生实例化对象,并且在普通类中可以包含有构造方法、普通方法、static方法、常量和变量等内容。而抽象类是指在普通类的结构里面增加抽象方法的组成部分。
那么什么叫抽象方法呢?在所有的普通方法上面都会有一个“{}”,这个表示方法体,有方法体的方法一定可以被对象直接使用。而抽象方法,是指没有方法体的方法,同时抽象方法还必须使用关键字abstract做修饰。
而拥有抽象方法的类就是抽象类,抽象类要使用abstract关键字声明。
范例:定义一个抽象类
abstract class A{//定义一个抽象类
public void fun(){//普通方法
System.out.println("存在方法体的方法");
}
public abstract void print();//抽象方法,没有方法体,有abstract关键字做修饰
}
主要特点
-
不能被实例化:抽象类不能直接通过
new
关键字来创建对象。它的主要目的是被其他类继承,然后由这些子类来实现它的抽象方法。 -
包含抽象方法:抽象类中可以包含抽象方法,抽象方法是只有方法声明而没有具体实现的方法。这些方法必须由抽象类的子类提供具体的实现。注意,一个类只要包含一个抽象方法,它就必须被声明为抽象类。但是,抽象类也可以不包含任何抽象方法,只作为基类使用。
-
作为基类:抽象类主要用于被其他类继承,为子类提供一个通用的类型,并强制子类实现某些特定的方法。这有助于实现代码的复用和类型的强制。
-
可以有具体实现的方法:抽象类不仅可以包含抽象方法,还可以包含具体实现的方法。这些方法在抽象类中就有具体的实现,子类可以直接继承使用,也可以根据需要重写这些方法。
使用场景
- 当你想定义一个接口,但又想包含一些已经实现的方法时,可以使用抽象类。
- 当你希望创建一个类的框架,让子类在这个框架的基础上实现具体的功能时,可以使用抽象类。
- 当你想要限制类的实例化,只能通过继承来实现类的对象时,可以使用抽象类。
六、接口
接口(interface)是抽象方法和常量值的定义的集合。
从本质上讲,接口是一种特殊的抽象类,这种抽象类中只包含常量和方法的定义,而没有变量和方法的实现。
接口的特点:
-
抽象性:接口中的所有方法都是抽象的,即它们都没有实现体(即没有方法体的大括号
{}
)。这意呀着实现接口的类必须提供这些方法的具体实现。 -
纯粹性:接口只包含方法声明,不包含任何成员变量(尽管在Java 8及以后,接口可以包含默认方法和静态方法,但这些方法仍然是抽象的,并且接口中不能有实例变量)。
-
多实现性:一个类可以实现多个接口,这允许一个类从多个源继承行为。
-
多继承的替代:在Java等不支持多重继承的语言中,接口提供了一种模拟多重继承的方式。一个类可以继承自一个类(实现单继承),但可以实现多个接口(实现多接口)。
-
解耦:接口有助于实现解耦,因为它们定义了类应该做什么,而不是如何做。这允许在不修改使用接口的代码的情况下,更换接口的实现。
举例
// 定义一个接口
interface Animal {
void eat();
void sleep();
}
// 实现接口的类
class Dog implements Animal {
@Override
public void eat() {
System.out.println("Dog is eating");
}
@Override
public void sleep() {
System.out.println("Dog is sleeping");
}
}
// 使用接口
public class TestInterface {
public static void main(String[] args) {
Animal myDog = new Dog(); // 使用接口引用指向Dog对象
myDog.eat();
myDog.sleep();
}
}
在这个例子中,Animal
接口定义了两个方法eat
和sleep
,Dog
类实现了这个接口,并提供了这两个方法的具体实现。然后,我们通过一个Animal
类型的引用来指向一个Dog
对象,并调用了这两个方法。这展示了接口如何允许我们编写与特定实现无关的代码。