面向对象编程知识小结
1、简要介绍
Java是一种面向对象的编程语言。面向对象编程,英文是Object-Oriented Programming
,简称OOP
。面向对象编程,是一种通过对象的方式,把现实世界映射到计算机模型的一种编程方法。
2、基本概念
2.1 类
class是一种对象模版,它定义了如何创建实例。而instance是对象实例,instance是根据class创建的实例。
- 定义person类
class Person {
public String name;
public int age;
}
2.2实例
用new操作符创建一个实例,然后,我们需要定义一个引用类型的变量来指向这个实例:
代码语言:javascript复制Person ming = new Person();
上述代码创建了一个Person
类型的实例,并通过变量ming
指向它。
2.3. 方法
2.3.1简要介绍
一个class可以包含多个属性,例如,我们给Person类就定义了两个属性:name和age。但是,直接把属性(field)用public暴露给外部可能会破坏封装性。所以需要结合使用修饰符public、private、protected,然后定义方法,实现类的封装。
代码语言:javascript复制public class Oop {
public static void main(String[] args) {
Person ming = new Person();
ming.name = "Xiao Ming"; // 对字段name赋值
ming.age = 12; // 对字段age赋值
}
}
class Person {
private String name;
private int age;
}
例如上方代码中,将person的成员变量用private修饰,但未设置成员方法访问,在实例化对象后直接报错:
所以这个时候,为了既实现封装,又能访问、操作class的私有属性,就需要为类添加public修饰的方法。具体如下代码所示:
代码语言:javascript复制public class Main {
public static void main(String[] args) {
Person ming = new Person();
ming.setName("Xiao Ming"); // 设置name
ming.setAge(12); // 设置age
System.out.println(ming.getName() ", " ming.getAge());
}
}
class Person {
private String name; //私有属性
private int age;
public String getName() { //成员方法
return this.name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return this.age;
}
public void setAge(int age) {
if (age < 0 || age > 100) {
throw new IllegalArgumentException("invalid age value");
}
this.age = age;
}
}
2.3.2 this变量
在方法内部,可以使用一个隐含的变量this,它始终指向当前实例。因此,通过this.field就可以访问当前实例的字段。
2.3.3方法参数
方法可以包含0个或任意个参数。方法参数用于接收传递给方法的变量值。调用方法时,必须严格按照参数的定义一一传递。
代码语言:javascript复制//示例代码
class Person {
...
public void setNameAndAge(String name, int age) {//两个参数
...
}
}
2.3.4构造方法
创建实例的时候,实际上是通过构造方法来初始化实例的。如下类中的有两个参数的构造方法。
代码语言:javascript复制class Person {
private String name;
private int age;
public Person(String name, int age) {//两个参数的构造方法
this.name = name;
this.age = age;
}
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
}
构造方法的名称就是类名。构造方法的参数没有限制,在方法内部,也可以编写任意语句。但是,和普通方法相比,构造方法没有返回值(也没有void),调用构造方法,必须用new操作符。 任何类都有构造方法,如果一个类没有定义构造方法,编译器会自动为我们生成一个默认构造方法,它没有参数,也没有执行语句,例如:
代码语言:javascript复制class Person {
public Person() { //默认的构造方法,如果不显式定义构造方法的,编译器会自动生成一个类似这样的构造方法。
}
}
要特别注意的是,如果我们自定义了一个构造方法,那么,编译器就不再自动创建默认构造方法如下示例中,就会产生错误:
代码语言:javascript复制public class Main {
public static void main(String[] args) {
Person p = new Person(); // 编译错误:找不到这个构造方法
}
}
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
}
错误如下:
总之,想要使用什么样的构造方法,要自己显式定义出来,并且可以定义多个构造方法。
2.3.5方法重载
在一个类中,我们可以定义多个方法。如果有一系列方法,它们的功能都是类似的,只有参数有所不同,那么,可以把这一组方法名做成同名方法。例如,在Hello类中,定义多个hello()方法:
代码语言:javascript复制 public void hello() {
//方法名相同,参数不同。
System.out.println("Hello, world!");
}
public void hello(String name) {
System.out.println("Hello, " name "!");
}
public void hello(String name, int age) {
if (age < 18) {
System.out.println("Hi, " name "!");
} else {
System.out.println("Hello, " name "!");
}
}
注意:方法重载的返回值类型通常都是相同的。 方法重载的目的是,功能类似的方法使用同一名字,更容易记住,因此,调用起来更简单。
3、实现方式
3.1 继承
继承是面向对象编程中非常强大的一种机制,它首先可以复用代码。Java使用extends关键字来实现继承:
代码语言:javascript复制class Person {
private String name;
private int age;
public String getName() {...}
public void setName(String name) {...}
public int getAge() {...}
public void setAge(int age) {...}
}
class Student extends Person {
// 不要重复name和age字段/方法,
// 只需要定义新增score字段/方法:
private int score;
public int getScore() { … }
public void setScore(int score) { … }
}
注意:
- 通过继承,Student只需要编写额外的功能,不再需要重复代码。
- 在OOP的术语中,我们把Person称为超类(super class),父类(parent class),基类(base class),把Student称为子类(subclass),扩展类(extended class)。
- Java只允许一个class继承自一个类,因此,一个类有且仅有一个父类。只有Object特殊,它没有父类。
- 继承有个特点,就是子类无法访问父类的private字段或者private方法。为了让子类可以访问父类的字段,我们需要把private改为protected。
- 用protected修饰的字段可以被子类访问。protected关键字可以把字段和方法的访问权限控制在继承树内部,一个protected字段和方法可以被其子类,以及子类的子类所访问
- super关键字表示父类(超类)。子类引用父类的字段时,可以用super.fieldName。
- 正常情况下,只要某个class没有final修饰符,那么任何类都可以从该class继承。 在Java中,任何class的构造方法,第一行语句必须是调用父类的构造方法。如果没有明确地调用父类的构造方法,编译器会帮我们自动加一句super();
class Student extends Person {
protected int score;
public Student(String name, int age, int score) {
super(); // 自动调用父类的构造方法
/*
如果父类没有无参的构造方法,可以在super()中添加已定义且与构造方法相同的参数列表,如下所示
super(name, age); 调用父类的构造方法Person(String, int)
*/
this.score = score;
}
}
结论:如果父类没有默认的构造方法,子类就必须显式调用super()并给出参数以便让编译器定位到父类的一个合适的构造方法。子类不会继承任何父类的构造方法。子类默认的构造方法是编译器自动生成的,不是继承的。
从Java 15开始,允许使用sealed修饰class,并通过permits明确写出能够从该class继承的子类名称。 示例:
代码语言:javascript复制public sealed class Shape permits Rect, Circle, Triangle {
...
}
上述Shape类就是一个sealed类,它只允许指定的3个类继承它。 如果写:
代码语言:javascript复制public final class Rect extends Shape {...}
是没问题的,因为Rect出现在Shape的permits列表中。 但是如果不是shape指定的三个类,则会报错,如:
代码语言:javascript复制public final class Ellipse extends Shape {...}
// Compile error: class is not allowed to extend sealed class: Shape
- 向上转型 把一个子类类型安全地变为父类类型的赋值,被称为向上转型(upcasting)。向上转型实际上是把一个子类型安全地变为更加抽象的父类型:
Student s = new Student(); //student 继承 person
Person p = s; // upcasting, ok
Object o1 = p; // upcasting, ok
Object o2 = s; // upcasting, ok
- 向下转型 和向上转型相反,如果把一个父类类型强制转型为子类类型,就是向下转型(downcasting).例如:
Person p1 = new Student(); // upcasting, ok
Person p2 = new Person();
Student s1 = (Student) p1; // ok
Student s2 = (Student) p2; // runtime error! ClassCastException!
Person类型p1实际指向Student实例,Person类型变量p2实际指向Person实例。在向下转型的时候,把p1转型为Student会成功,因为p1确实指向Student实例,把p2转型为Student会失败,因为p2的实际类型是Person,不能把父类变为子类,因为子类功能比父类多,多的功能无法凭空变出来。 因此,向下转型很可能会失败。失败的时候,Java虚拟机会报ClassCastException。 为了避免向下转型出错,Java提供了instanceof操作符,可以先判断一个实例究竟是不是某种类型:
代码语言:javascript复制Person p = new Person();
System.out.println(p instanceof Person); // true
System.out.println(p instanceof Student); // false
Student s = new Student();
System.out.println(s instanceof Person); // true
System.out.println(s instanceof Student); // true
Student n = null;
System.out.println(n instanceof Student); // false
instanceof实际上判断一个变量所指向的实例是否是指定类型,或者这个类型的子类。如果一个引用变量为null,那么对任何instanceof的判断都为false。
3.2多态
在继承关系中,子类如果定义了一个与父类方法签名完全相同的方法,被称为覆写(Override)。 例如,在Person类中,我们定义了run()方法:
代码语言:javascript复制class Person {
public void run() {
System.out.println("Person.run");
}
}
在子类Student中,覆写这个run()方法:
class Student extends Person {
@Override
public void run() {
System.out.println("Student.run");
}
}
Override和Overload不同的是,如果方法签名不同,就是Overload,Overload方法是一个新方法;如果方法签名相同,并且返回值也相同,就是Override。
加上@Override可以让编译器帮助检查是否进行了正确的覆写,@Override不是必需的。希望进行覆写,但是不小心写错了方法签名,编译器会报错。如下所示:
代码语言:javascript复制public class Main {
public static void main(String[] args) {
}
}
class Person {
public void run() {}
}
public class Student extends Person {
@Override // Compile error!
public void run(String s) {}
}
Java的实例方法调用是基于运行时的实际类型的动态调用,而非变量的声明类型。 这个非常重要的特性在面向对象编程中称之为多态。它的英文拼写非常复杂:Polymorphic。
- 定义 多态是指,针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法。多态的特性就是,运行期才能动态决定调用的子类方法。对某个类型调用某个方法,执行的实际方法可能是某个子类的覆写方法.
- 在子类的覆写方法中,如果要调用父类的被覆写的方法,可以通过super来调用。
- 继承可以允许子类覆写父类的方法。如果一个父类不允许子类对它的某个方法进行覆写,可以把该方法标记为final。用final修饰的方法不能被Override.
- 如果一个类不希望任何其他类继承自它,那么可以把这个类本身标记为final。用final修饰的类不能被继承
- 对于一个类的实例字段,同样可以用final修饰。用final修饰的字段在初始化后不能被修改。