前言
与C语言面向过程编程不同,JAVA中面向对象的编程更符合人类大脑的思维模式。
面向对象的本质就是: 以类的方式组织代码,以对象的组织(封装)数据。
对象,是具体的事物。类,是抽象的,是对对象的抽象。
从代码运行角度考虑是先有类后有对象。
==类是对象的模板。==
一、包
包 (package) 是组织类的一种方式。
使用包的主要目的是保证类的唯一性。
例如, 你在代码中写了一个 Test 类. 然后你的同事也可能写一个 Test 类. 如果出现两个同名的类, 就会冲突, 导致代码不能编译通过.
1.1 导入包中的类
Java 中已经提供了很多现成的类供我们使用。
public class Test {
public static void main(String[] args) {
java.util.Date date = new java.util.Date();
// 得到一个毫秒级别的时间戳
System.out.println(date.getTime());
}
}
可以使用 java.util.Date 这种方式引入 java.util 这个包中的 Date 类.
但是这种写法比较麻烦一些, 可以使用 import 语句导入包.
import java.util.Date;
public class Test {
public static void main(String[] args) {
Date date = new Date();
// 得到一个毫秒级别的时间戳
System.out.println(date.getTime());
}
}
如果需要使用 java.util 中的其他类, 可以使用 import java.util.*
import java.util.*;
public class Test {
public static void main(String[] args) {
Date date = new Date();
// 得到一个毫秒级别的时间戳
System.out.println(date.getTime());
}
}
==注意:import导入的无论如何都是类,而不能理解成导入了一个包!!这里的*号指的是根据实际情况调用需要的类。==
特殊情况:但是我们更建议显式的指定要导入的类名. 否则还是容易出现冲突的情况:
import java.util.*;
import java.sql.*;
public class Test {
public static void main(String[] args) {
// util 和 sql 中都存在一个 Date 这样的类, 此时就会出现歧义, 编译出错
Date date = new Date();
System.out.println(date.getTime());
}
}
==此时编译出错!!!
Error:(5, 9) java: 对Date的引用不明确
java.sql 中的类 java.sql.Date 和 java.util 中的类 java.util.Date 都匹配==
在这种情况下需要使用完整的类名:
import java.util.*;
import java.sql.*;
public class Test {
public static void main(String[] args) {
java.util.Date date = new java.util.Date();
System.out.println(date.getTime());
}
}
1.2 静态导入
使用 import static 可以导入包中的静态的方法和字段
import static java.lang.System.*;
public class Test {
public static void main(String[] args) {
out.println("hello");
}
}
使用这种方式可以更方便的写一些代码, 例如:
import static java.lang.Math.*;
public class Test {
public static void main(String[] args) {
double x = 30;
double y = 40;
// 静态导入的方式写起来更方便一些.
// double result = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
double result = sqrt(pow(x, 2) + pow(y, 2));
System.out.println(result);
}
}
1.3 包的访问控制权限
1.4 常见的系统包
- java.lang: 系统常用基础类(String、Object),此包从JDK1.1后自动导入。
- java.lang.reflect: java 反射编程包;
- java.net: 进行网络编程开发包。
- java.sql: 进行数据库开发的支持包。
- java.util: 是java提供的工具程序包。(集合类等) 非常重要
- java.io: I/O编程开发包。
二、继承
代码中创建的类, 主要是为了抽象现实中的一些事物(包含属性和方法).
有的时候客观事物之间就存在一些关联关系, 那么在表示成类和对象的时候也会存在一定的关联.
2.1 背景
当类和类之间满足:一个类is a 另一个类,一定存在继承关系。
比如: Bird is an Animal. Cat is an Animai.所以鸟和猫与动物有天然的继承关系。
子类会继承父类的字段和方法,以达到代码重用的功能。
2.2 语法规则
基本语法:
class 子类 extends 父类 {
}
==注意事项:(重点!!!!)==
1.使用 extends 指定父类.
2.Java 中一个子类只能继承一个父类 (而C++/Python等语言支持多继承).
3.子类会继承父类的所有 public 的字段和方法.
4.对于父类的 private 的字段和方法, 子类中是无法访问的(但其实是隐式继承了,只是没办法调用).
5.子类的实例中, 也包含着父类的实例. 可以使用 super 关键字得到父类实例的引用。
6.new一个子类的时候,会先调用父类的构造块和构造方法。(所以在类加载的时候也会先加载父类,调用父类的静态代码块,然后才到子类的静态代码块)
7.Java虽然只能单继承,但允许多层继承。
例如:
现有两个类,主函数执行之后:
这是顺序:
2.3 protected关键字
刚才我们发现, 如果把字段设为 private, 子类不能访问. 但是设成 public, 又违背了我们 "封装" 的初衷.
两全其美的办法就是 protected 关键字。
==**1.对于类的调用者来说, protected 修饰的字段和方法是不能访问的==
==2.对于类的 子类 和 同一个包的其他类 来说, protected 修饰的字段和方法是可以访问的**==
// Animal.java
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void eat(String food) {
System.out.println(this.name + "正在吃" + food);
}
}
// Bird.java
public class Bird extends Animal {
public Bird(String name) {
super(name);
}
public void fly() {
// 对于父类的 protected 字段, 子类可以正确访问
System.out.println(this.name + "正在飞 ︿( ̄︶ ̄)︿");
}
}
// Test.java 和 Animal.java 不在同一个 包 之中了.
public class Test {
public static void main(String[] args) {
Animal animal = new Animal("小动物");
System.out.println(animal.name); // 此时编译出错, 无法访问 name
}
}
==小结: Java 中对于字段和方法共有四种访问权限:==
private: 类内部能访问, 类外部不能访问
默认(也叫包访问权限): 类内部能访问, 同一个包中的类可以访问, 其他类不能访问.
protected: 类内部能访问, 子类和同一个包中的类可以访问, 其他类不能访问.
public : 类内部和类的调用者都能访问
2.4 final关键字
我们并不希望类之间的继承层次太复杂. 一般我们不希望出现超过三层的继承关系. 如果继承层次太多, 就需要考虑对代码进行重构了。
如果想从语法上进行限制继承, 就可以使用final关键字。
曾经我们学习过 final 关键字, 修饰一个变量或者字段的时候, 表示 常量 (不能修改)。
final int a = 10;
a = 20; // 编译出错
final 关键字也能修饰类, 此时表示被修饰的类就不能被继承。
final public class Animal {
...
}
public class Bird extends Animal {
...
}
// 编译出错
Error:(3, 27) java: 无法从最终com.bit.Animal进行继承
final 关键字的功能是 限制 类被继承
"限制" 这件事情意味着 "不灵活". 在编程中, 灵活往往不见得是一件好事. 灵活可能意味着更容易出错.
是用 final 修饰的类被继承的时候, 就会编译报错, 此时就可以提示我们这样的继承是有悖这个类设计的初衷的
我们平时是用的 String 字符串类, 就是用 final 修饰的, 不能被继承。
## 2.5 super关键字
1.super先从直接父类中寻找同名属性,若不存在再向上寻找。
2.this直接从当前类中寻找同名属性,若不存在再向上搜索。
3.若父类中不存在无参构造,则子类构造方法的首行必须使用super(有参构造)
4.在一个构造方法中无法显式使用this()和super()同时出现。
- super不能指代当前父类的引用
==6.当父类中只有有参构造方法时,子类构造方法的首行必须显式使用super调用父类这个有参构造,否则会报错。(重要)==
三、多态
一个引用可以表现出多种行为/特性 --> 多态性
3.1 向上转型
3.1.1 语法规则
形如如下例子:
Bird bird = new Bird("圆圆");
这个代码也可以写成:
Bird bird = new Bird("圆圆");
Animal bird2 = bird;
// 或者写成下面的方式
Animal bird2 = new Bird("圆圆");
此时 bird2 是一个父类 (Animal) 的引用, 指向一个子类 (Bird) 的实例. 这种写法称为 向上转型。
==向上转型发生在有继承的类之间==
==父类名称 父类引用=new 子类实例();==
3.1.2 向上转型发生的时机
==1.直接赋值==
直接赋值就是如上代码演示。
==2.方法传参==
public class Test {
public static void main(String[] args) {
Bird bird = new Bird("圆圆");
feed(bird);
}
public static void feed(Animal animal) {
animal.eat("谷子");
}
}
// 执行结果
圆圆正在吃谷子
此时形参 animal 的类型是 Animal (基类), 实际上对应到 Bird (父类) 的实例。
==3.方法返回==
public class Test {
public static void main(String[] args) {
Animal animal = findMyAnimal();
}
public static Animal findMyAnimal() {
Bird bird = new Bird("圆圆");
return bird;
}
}
此时方法 findMyAnimal 返回的是一个 Animal 类型的引用, 但是实际上对应到 Bird 的实例。
3.1.3 向上转型的意义
最大的意义在于参数统一化,降低使用者的使用难度!!!
3.2 动态绑定
在 Java 中, 调用某个类的方法, 究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) , 要看究竟这个引用指向的是父类对象还是子类对象. 这个过程是程序运行时决定的(而不是编译期), 因此称为 动态绑定.
// Animal.java
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void eat(String food) {
System.out.println("我是一只小动物");
System.out.println(this.name + "正在吃" + food);
}
}
// Bird.java
public class Bird extends Animal {
public Bird(String name) {
super(name);
}
public void eat(String food) {
System.out.println("我是一只小鸟");
System.out.println(this.name + "正在吃" + food);
}
}
// Test.java
public class Test {
public static void main(String[] args) {
Animal animal1 = new Animal("圆圆");
animal1.eat("谷子");
Animal animal2 = new Bird("扁扁");
animal2.eat("谷子");
}
}
// 执行结果
我是一只小动物
圆圆正在吃谷子
我是一只小鸟
扁扁正在吃谷子
3.3 方法重写
==方法重载(overload):==
发生在同一个类中,定义了若干个方法名称相同,参数列表不同的一组方法。
==方法重写(override):==
发生在有继承关系的类之间,子类定义了和父类除了权限不同,其他全都相同的方法,这样一组方法称为方法重写。
Duck重写了Bird的eat方法~~~
==关于重写的注意事项:==
- 重写和重载完全不一样. 不要混淆(思考一下, 重载的规则是啥?)
- 普通方法可以重写, static 修饰的静态方法不能重写
- 重写中子类的方法的访问权限不能低于父类的方法访问权限.
例如:
小于则报错:
- 重写的方法返回值类型不一定和父类的方法相同(但是建议最好写成相同, 特殊情况除外)
==这里的特殊情况指的是,可以定义父类的返回值类型,但return了一个子类的类型。==
- 另外, 针对重写的方法, 可以使用 @Override 注解来显式指定
// Bird.java
public class Bird extends Animal {
@Override
private void eat(String food) {
...
}
}
有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写
3.4 多态的好处
1) 类调用者对类的使用成本进一步降低
封装是让类的调用者不需要知道类的实现细节.
多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可.
因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低.
这也贴合了 <<代码大全>> 中关于 "管理代码复杂程度" 的初衷.
2) 能够降低代码的 "圈复杂度", 避免使用大量的 if - else
例如我们现在需要打印的不是一个形状了, 而是多个形状. 如果不基于多态, 实现代码如下:
public static void drawShapes() {
Rect rect = new Rect();
Cycle cycle = new Cycle();
Flower flower = new Flower();
String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};
for (String shape : shapes) {
if (shape.equals("cycle")) {
cycle.draw();
} else if (shape.equals("rect")) {
rect.draw();
} else if (shape.equals("flower")) {
flower.draw();
}
}
}
如果使用使用多态, 则不必写这么多的 if - else 分支语句, 代码更简单:
public static void drawShapes() {
// 我们创建了一个 Shape 对象的数组.
Shape[] shapes = {new Cycle(), new Rect(), new Cycle(),
new Rect(), new Flower()};
for (Shape shape : shapes) {
shape.draw();
}
}
什么叫 "圈复杂度" ?
圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如果有很
多的条件分支或者循环语句, 就认为理解起来更复杂.
因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 "圈复杂度". 如果一
个方法的圈复杂度太高, 就需要考虑重构
3) 可扩展能力更强.
如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低.
class Triangle extends Shape {
@Override
public void draw() {
System.out.println("△");
}
}
对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低。
而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高。
3.5 向下转型
==向上转型是子类对象转成父类对象, 向下转型就是父类对象转成子类对象. 相比于向上转型来说, 向下转型没那么常见,但是也有一定的用途==
// Animal.java
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void eat(String food) {
System.out.println("我是一只小动物");
System.out.println(this.name + "正在吃" + food);
}
}
// Bird.java
public class Bird extends Animal {
public Bird(String name) {
super(name);
}
public void eat(String food) {
System.out.println("我是一只小鸟");
System.out.println(this.name + "正在吃" + food);
}
public void fly() {
System.out.println(this.name + "正在飞");
}
}
接下来是我们熟悉的操作
Animal animal = new Bird("圆圆");
animal.eat("谷子");
// 执行结果
圆圆正在吃谷子
接下来我们尝试让圆圆飞起来
animal.fly();
// 编译出错
找不到 fly 方法
注意事项
1.编译过程中, animal 的类型是 Animal, 此时编译器只知道这个类中有一个 eat 方法, 没有 fly 方法.
2.虽然 animal 实际引用的是一个 Bird 对象, 但是编译器是以 animal 的类型来查看有哪些方法的.
3.对于 Animal animal = new Bird("圆圆") 这样的代码,编译器检查有哪些方法存在, 看的是 Animal 这个类型。
4.执行时究竟执行父类的方法还是子类的方法, 看的是 Bird 这个类型.
那么想实现刚才的效果, 就需要向下转型。
// (Bird) 表示强制类型转换
Bird bird = (Bird)animal;
bird.fly();
// 执行结果
圆圆正在飞
但是这样的向下转型有时是不太可靠的. 例如:
Animal animal = new Cat("小猫");
Bird bird = (Bird)animal;
bird.fly();
// 执行结果, 抛出异常
Exception in thread "main" java.lang.ClassCastException: Cat cannot be cast to Bird at Test.main
animal 本质上引用的是一个 Cat 对象是不能转成 Bird 对象的. 运行时就会抛出异常。所以, 为了让向下转型更安全, 我们可以先判定一下看看 animal 本质上是不是一个 Bird 实例, 再来转换。
Animal animal = new Cat("小猫");
if (animal instanceof Bird) {
Bird bird = (Bird)animal;
bird.fly();
}
==instanceof 可以判定一个引用是否是某个类的实例. 如果是, 则返回 true. 这时再进行向下转型就比较安全了==
总结
多态是面向对象程序设计中比较难理解的部分. 我们会在下篇的抽象类和接口中进一步体会多态的使用. 重点是多态带来的编码上的好处。
另一方面, 如果抛开 Java, 多态其实是一个更广泛的概念, 和 "继承" 这样的语法并没有必然的联系
==✦C++ 中的 "动态多态" 和 Java 的多态类似. 但是 C++ 还有一种 "静态多态"(模板), 就和继承体系没有关系了。==
==✦Python 中的多态体现的是 "鸭子类型", 也和继承体系没有关系。==
==✦Go 语言中没有 "继承" 这样的概念, 同样也能表示多态。==
无论是哪种编程语言, 多态的核心都是让调用者不必关注对象的具体类型. 这是降低用户使用成本的一种重要方式。