第二章-面向对象程序设计
1. 什么是对象
1.1 面向对象的思想
◼ 面向过程编程
Procedure Oriented Programming,流行于60年代; 分析出解决问题的步骤,然后用函数逐一实现每一步,再依次调用这些函数。
◼ 面向对象编程
Object Oriented Programming,从70年代流行至今; 分析出系统中应包含哪些对象,以及它们之间的关系,再用特定语法描述这些对象和关系。面向对象,也称为面向物体,是通过抽象一个物体来构思结构的,从而解决问题。
◼ 类和对象的关系
- 类是“设计图”,用于描述一批对象的信息,它是抽象的,不是具体的实例;
- 对象是具体的,是根据类的描述,由JVM创建出来的一个实例,这个过程叫实例化;
- 类是自定义的数据类型,通过这个类创建对象,实际上就是初始化这种类型的变量。
1.2 描述对象
◼ 属性
对象的静态特征; 用变量来描述,这样的变量叫成员变量。
◼ 行为
对象的动态特征; 用方法来描述,这样的方法叫成员方法。
1.3 成员方法详解
◼ 一个成员方法,可以直接访问当前类中所有的成员变量;
◼ 访问成员方法 一个成员方法,可以直接访问当前类中所有的成员方法;
◼ 成员方法参数 可以是基本数据类型,也可以是引用数据类型,包括自定义的类型。
例子:
我们定义一个赛车游戏,需要先定义两个类,分别是 车 和 赛车手
// 赛车手 public class Driver { String name; int age; char sex; // 自我介绍 void introduce() { System.out.println("我叫" + name); System.out.println("我今年" + age + "岁"); System.out.println("我是" + sex + "性"); } // 驾驶赛车 void drive(Car car) { System.out.println(name + "驾驶" + car.color + car.brand); car.run(); } } public class Car { String brand; String color; int maxSpeed; // 行驶 void run() { // 方法内声明的变量叫局部变量,只能在当前方法内访问. // 调用成员变量 System.out.println(color + brand + "时速" + maxSpeed); // 调用成员方法 startup(); speedUp(); stop(); } // 启动 void startup() { System.out.println(brand + "启动!"); } // 加速 void speedUp() { System.out.println(brand + "加速!"); } // 刹车 void stop() { System.out.println(brand + "刹车!"); } }
然后在main方法中,常见两个车,和一个赛车选手的对象
public static void main(String[] args) { Car c1 = new Car(); c1.brand = "奔驰"; c1.color = "红色"; c1.maxSpeed = 500; c1.run(); Car c2 = new Car(); c2.brand = "宝马"; c2.color = "蓝色"; c2.maxSpeed = 400; c2.run(); Driver d = new Driver(); d.name = "悟空"; d.age = 25; d.sex = '男'; d.introduce(); d.drive(c1); d.drive(c2); }
1.4 JVM 简单介绍
JVM启动时会向系统申请一块内存,它将这块内存划分为若干个子区域,用以存放不同形式的数据。其中堆内存用来存放引用类型的数据,栈以方法为单元存储数据,这样的单元叫方法栈帧。
以赛车程序为例:
1.创建对象Car c = new Car();
new Car()会使得堆区分配一片内存区域给这个新对象,Car c 会在main的栈帧中创建一个变量,存储刚才新创建的对象的引用地址。
2.对象赋值
给对象的成员变量赋值,其中"奔驰"是字符串,实际内存分配的是Byte[4],一个字节数组,里面有4个元素,每两个表示一个汉字
3.成员方法调用
调用对象的run
方***在栈中压入新的栈帧,也就是run
方法,在run
方法中,还会调用startup
方法,也会让它也进入栈帧中
在debug环境中,可以初步观察到,随着方法的调用,会有新的方法入栈,随着这个方法的结束,也会出栈。
2.对象的方法
2.1 再谈方法的参数
public class Game { public static void main(String[] args) { Driver d = new Driver(); double money = 8000.0; d.earn(money); System.out.println("发放: " + money + "元!"); Car c = new Car(); c.color = "红色"; d.refit(c); System.out.println("颜色: " + c.color); } } public class Driver { void earn(double money) { money /= 2; System.out.println("收入: " + money + "元!"); } void refit(Car car) { car.color = "蓝色"; } int lucky() { int k = (int) (Math.random() * 100); return k; } }
1.基本类型参数
在main方法中有变量double型变量money, d.earn()方法传入money, 在Driver类中的方法earn中,修改money, 但实际上不会对main方法中的money产生影响,因为它们是基本数据类型,在各自的栈帧中互不影响。
2.引用参数类型
在main方法中,存放有指向d和c对象的引用变量,当调用d.refit()时,会创建这个方法的栈帧,并会传入c对象的引用到这个方法栈帧中,在refit的栈帧中对c对象的修改是会影响到main方法栈帧中的c对象,因为它们都指向一个内存空间。
2.2 方法返回
public class Game { public static void main(String[] args) { Driver d = new Driver(); int n = d.lucky(); System.out.println("幸运数字: " + n); } } public class Driver { int lucky() { int k = (int) (Math.random() * 100);![图片说明](https://uploadfiles.nowcoder.com/images/20200901/45741152_1598972970925_D18C2E413FF4CB188F58209CEAE63BDD "图片标题") // return返回的机制是什么 return k; } }
在main函数中,调用d.lucky()后,采用debug模式进入lucky()函数内部,可以看到return k 是把k的值,先放到main函数中的专门承接返回值的临时变量中,然后把这个值再赋给n,随后销毁这个临时变量
2.3 构造方法
方便我们创建和初始化一个对象
- 方法名必须与类名相同;
- 构造方法不能有返回值;
- 当显示定义构造方法时,编译器将不再生成默认的构造方法。
构造出场日期的唯一编号,System.currentTimeMillis();
返回当前时刻
2.4 成员方法重载
优雅的编程,利用传入参数的不同自动区分函数的不同
在案例中展示,一个司机类调用drive方法,根据传入类的不同,都可以驾驶相应的汽车
public class Driver { @Override void drive(Car car) { car.run(); } @Override void drive(Truck truck) { truck.run(); } } public class Game { public static void main(String[] args) { Driver d = new Driver(); Car c = new Car(); d.drive(c); Truck t = new Truck(); d.drive(t); } }
2.5 构造方法重载
在构造器中,通过顺序缺省参数,达到灵活初始化类的目的
2.6 this 关键字
这样写太慢了,直接写最关键的内容了,结合免费的java入门教程
◼ this关键字的含义 this关键字用于指代当前对象。
◼ this关键字的用法
- 调用当前对象的构造方法;
- 调用当前对象的成员变量;
- 调用当前对象的成员方法;
最直观的作用就是在构造器中,方便程序区分成员变量和构造器传入的参数
public Car(String brand, String color, int maxSpeed) { //如果没有this,brand复制给brand,无法传递给这个对象的brand this.brand = brand; this.color = color; this.maxSpeed = maxSpeed; }
在调用当前对象的构造方法的时候,一定要注意this要写在第一行
public class Truck { String brand; String color; int capacity; public Truck(String brand) { this.brand = brand; } public Truck(String brand, String color) { // 必须放在第一行 // 可以复用之前的构造方式,代码简洁 this(brand); this.color = color; } public Truck(String brand, String color, int capacity) { // 必须放在第一行 this(brand, color); this.capacity = capacity; } }
2.7 三大特征
◼封装
隐藏实现细节,再提供特定的方法访问对象内部的信息,可以提高程序的安全性;
◼ 继承
继承于某一个类,就可以直接获得这个类的属性和方法,可以提高程序的复用性;
◼ 多态
程序运行时,在需要父类的地方均可以传入子类的对象,可以提高程序的扩展性。
2.8封装
参考教材内容
封装就是让类内部的属性私有化,防止外部类直接访问修改,对外部累提供了公有的setter和getter方法,这体现了良好的封装能够减少耦合,并且隐藏信息,实现细节。同时公有的setter和getter方法也让我们在使用构造体对类初始化赋值时能对输入数据进行处理,可以对成员变量进行更精确的控制,类内部的结构可以自由修改。
public class Driver { String name; private int age; public Driver(String name, int age){ this.name = name; // 可以清洗输入的年龄,让他合法化 this.setAge(age); } // 公有的set方法,用于赋值 public void setAge(int age){ if (age < 0){ this.age = 0; }else if (age > 200){ this.age = 200; } else{ this.age = age; } } // 公有的get方法,用于取值 public int getAge(){ return this.age; } }
2.9 继承
具体参考课本讲义
- Java采用extends关键字实现继承,实现继承的类叫子类,被继承的类叫父类;
- 任何类都只能有一个直接的父类,和无数个间接的父类,多个类可以继承于同一个父类;
- 若一个类没有显示指定父类,则这个类默认继承于java.lang.Object;
另外,从父类角度看,父类派生了子类,但从子类角度看,是子类扩展(extends)了父类。
2.10 重写
◼ 什么是重写
在子类中,定义与父类同名的方法,用来覆盖父类中这个方法的逻辑,叫做重写。
◼ 重写的规范
- 子类方法的名称、参数列表与父类相同;
- 子类方法的返回类型与父类相同或更小; 可以和父类一样或者特指子类
- 子类方法声明的异常与父类相同或更小;
- 子类方法的访问权限与父类相同或更大。可以从default变成public
2.11 super关键字
super关键字最直观的提出背景就是,我们在一个类的内部可以用this指代这个类,表示当前类对象引用。那如果想调用父类的方法或称成员变量呢?就是使用super关键字。
class Parent { void method1() { ... } } class Sub extends Parent { // 子类自己的method1方法(这也是重写) void method1() { ... } // 调用父类的method1方法,和子类的加以区分 void method2() { super.method1(); } }
super关键字的应用就是:
- 通过super关键字,调用父类的成员变量;
- 通过super关键字,调用父类的成员方法;
- 通过super关键字,调用父类的构造方法。
super关键字使用补充:
- super指代当前对象,用于调用该对象在父类中的成员;
- 通过super调用构造方法时,并不会创建一个新的对象;
- 父类构造方***在子类构造方法之前调用,并且总会被调用一次。
以上三点可以参考进阶文章中的讨论。