包 (package)
是组织类的一种方式,使用包的主要目的是保证类的唯一性。
Java中已经提供了很多现成的类供我们使用,比如我们可以使用 java.util.Date
这种方式引入 java.util
这个包中的 Date 类
。代码如下:
public class Test {
public static void main(String[] args) {
java.util.Date date = new java.util.Date();
// 得到一个毫秒级别的时间戳
System.out.println(date.getTime());
}
}
但是这种写法往往比较麻烦,所以我们可以使用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());
}
}
注意:Java是用到包中的哪个类就导入哪个类
但是 我们更建议显示的指定要导入的类名,否则还是容易出现冲突的情况。例如:
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());
}
}
注意事项:
- import 和 C++ 的 #include 差别很大。C++ 必须通过 #include 来引入其他文件内容, 但是 Java 不需要,
import 只是为了写代码的时候更方便
。import 更类似于 C++ 的 namespace 和 usingimport java.util.*
导入包下的所有类,Java是用到哪个类再去拿哪个类,而不是像include
一样导入所有的文件
使用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);
}
}
基本规则:
域名的颠倒形式
(例如 com.bit.demo1 )代码路径
相匹配。例如创建 com.bit.demo1 的包, 那么会存在一个对应的路径 com/bit/demo1 来存储代码默认包
中我们已经了解了类中的 public 和 private, private 中的成员只能被类的内部
使用
如果某个成员不包含 public 和 private 关键字, 此时这个成员可以在包内部的其他类
使用, 但是不能在包外部的类使用
下面的代码给了一个示例。 Demo1 和 Demo2 是同一个包中, Test 是其他包中:
Demo1.java:
package com.bit.demo;
public class Demo1 {
int value = 0;
}
Demo2.java:
package com.bit.demo;
public class Demo2 {
public static void Main(String[] args) {
Demo1 demo = new Demo1();
System.out.println(demo.value);
}
}
// 执行结果, 能够访问到 value 变量
10
Test.java:
import com.bit.demo.Demo1;
public class Test {
public static void main(String[] args) {
Demo1 demo = new Demo1();
System.out.println(demo.value);
}
}
// 编译出错
Error:(6, 32) java: value在com.bit.demo.Demo1中不是公共的; 无法从外部程序包中对其进行访问
java.lang
:系统常用基础类(String、Object),此包从JDK1.1后自动导入。java.lang.reflect
:java 反射编程包java.net
:进行网络编程开发包。java.sql
:进行数据库开发的支持包。java.util
:是java提供的工具程序包。(集合类等) 非常重要java.io
:I/O编程开发包
简单来说,继承的意义就是
实现代码的复用
代码中创建的类, 主要是为了抽象现实中的一些事物(包含属性和方法)
有的时候客观事物之间就存在一些关联关系, 那么在表示成类和对象的时候也会存在一定的关联
例如, 设计一个类表示动物:
public String name;
public Animal(String name) {
this.name = name;
}
public void eat(String food) {
System.out.println(this.name + "正在吃" + food);
}
}
class Cat {
public String name;
public Cat(String name) {
this.name = name;
}
public void eat(String food) {
System.out.println(this.name + "正在吃" + food);
}
}
class Bird {
public String name;
public Bird(String name) {
this.name = name;
}
public void eat(String food) {
System.out.println(this.name + "正在吃" + food);
}
public void fly() {
System.out.println(this.name + "正在飞 ︿( ̄︶ ̄)︿");
}
}
这个代码我们发现其中存在了大量的冗余代码
仔细分析, 我们发现 Animal
和 Cat
以及 Bird
这几个类中存在一定的关联关系:
is - a 语义
)此时我们就可以让 Cat 和 Bird 分别继承
Animal 类, 来达到代码重用的效果
基本语法:
class 子类 extends 父类 {
}
- 使用
extends
指定父类- Java 中
一个
子类只能继承一个
父类 (而C++/Python等语言支持多继承)- 子类会继承父类的所有
public
的字段和方法- 对于父类的
private
的字段和方法, 子类中是无法访问的- 子类的实例中, 也包含着父类的实例。 可以使用
super
关键字得到父类实例的引用
这时候,我们再把上面的代码改一下,通过 extends
关键字实现继承,将 Cat
和 Bird
继承 Animal
类, Cat 在定义的时候就不必再写 name 字段和 eat 方法:
class Animal {
public String name;
public Animal(String name) {
this.name = name;
}
public void eat(String food) {
System.out.println(this.name + "正在吃" + food);
}
}
class Cat extends Animal {
public Cat(String name) {
// 使用 super 调用父类的构造方法.
super(name);
}
}
class Bird extends Animal {
public Bird(String name) {
super(name);
}
public void fly() {
System.out.println(this.name + "正在飞 ︿( ̄︶ ̄)︿");
}
}
public class Test {
public static void main(String[] args) {
Cat cat = new Cat("小黑");
cat.eat("猫粮");
Bird bird = new Bird("圆圆");
bird.fly();
}
}
像Animal这种被继承的类,我们称为 父类、基类 或者 超类
,对于像 Cat 和 Bird 这样的类,我们称为 子类 或者 派生类
。和现实中的儿子继承父亲的财产类似, 子类也会继承父类的字段和方法
, 以达到代码重用的效果
这时候,如果我们把 name 改成 private, 那么此时子类就不能访问了:
public Bird(String name) {
super(name);
}
public void fly() {
System.out.println(this.name + "正在飞 ︿( ̄︶ ̄)︿");
}
}
// 编译出错
Error:(19, 32) java: name 在 Animal 中是 private 访问控制
刚才我们发现, 如果把字段设为 private, 子类不能访问。但是设成 public, 又违背了我们 “封装” 的初衷。两全其美的办法就是 protected
关键字
子类
和 同一个包的其他类
来说, 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
: 类内部和类的调用者都能访问封装
, 即隐藏内部实现细节, 只暴露出 必要
的信息给类的调用者。比较严格
的访问权限. 例如如果一个方法能用 private, 就尽量不要用 public 。类内部
自己用, 还是类的调用者
使用, 还是子类
使用)刚才我们的例子中, 只涉及到 Animal, Cat 和 Bird 三种类。但是如果情况更复杂一些呢?
针对 Cat 这种情况, 我们可能还需要表示更多种类的猫:
这个时候使用继承方式来表示, 就会涉及到更复杂的体系:
// Animal.java
public Animal {
...
}
// Cat.java
public Cat extends Animal {
...
}
// ChineseGardenCat.java
public ChineseGardenCat extends Cat {
...
}
// OrangeCat.java
public Orange extends ChineseGardenCat {
...
}
......
如刚才这样的继承方式称为多层继承
,即子类还可以进一步的再派生出新的子类。 一般我们不希望出现超过三层
的继承关系。 如果继承层次太多,就需要考虑对代码进行重构。
如果想从语法上进行限制继承, 就可以使用 final
关键字。
组合和继承类似,也是一种表达类之间关系
的方式,也是能够达到代码重用
的效果,例如表示一个学校:
public class Student {
...
}
public class Teacher {
...
}
public class School {
public Student[] students;
public Teacher[] teachers;
}
组合并没有涉及到特殊的语法(诸如 extends 这样的关键字),仅仅是将一个类的实例
作为另外一个类的字段
。这是我们设计类的一种常用方式之一。
组合和继承的区别:
- 组合表示
has - a
语义在刚才的例子中, 我们可以理解成一个学校中 “
包含
” 若干学生和教师
- 继承表示
is - a
语义在上面的 “动物和猫” 的例子中, 我们可以理解成一只猫也 “
是
” 一种动物
刚才例子中,我们写了这样一段代码:
Bird bird = new Bird("圆圆");
这段代码也可以这样写:
Bird bird = new Bird("圆圆");
Animal bird2 = bird;
// 或者写成下面的方式
Animal bird2 = new Bird("圆圆");
此时 bird2
是一个父类 (Animal) 的引用
,指向一个子类 (Bird) 的实例
。这种写法称为 向上转型
向上转型发生的时机主要有三种:
直接赋值的方式我们已经演示了,接下来我们具体演示一下其他两种:
方法传参:
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
的实例
方法返回:
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 的实例
当子类和父类中出现同名方法
的时候,再去调用会出现什么情况呢?
对前面的代码稍加修改, 给 Bird 类也加上同名的 eat 方法,并且在两个 eat 中分别加上不同的日志:
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);
}
}
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 class Test {
public static void main(String[] args) {
Animal animal1 = new Animal("圆圆");
animal1.eat("谷子");
Animal animal2 = new Bird("扁扁");
animal2.eat("谷子");
}
}
// 执行结果
我是一只小动物
圆圆正在吃谷子
我是一只小鸟
扁扁正在吃谷子
此时,我们发现:
首先我们找到class文件所在目录,按住shift,右键点击Test,点击PowerShell,发现编译时调用的方法并不能确定真正调用的方法
:
因此, 在 Java 中,调用某个类的方法,究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) , 要看究竟这个引用指向
的是父类对象还是子类对象。 这个过程是程序运行时决定
的(而不是编译期), 因此称为 动态绑定
子类实现父类的同名方法,并且
参数的类型
和个数
完全相同,这种情况称为覆写/重写/覆盖(Override)
注意事项:
普通方法可以重写, static
修饰的静态方法不能重写
重写中子类的方法的访问权限不能低于
父类的方法访问权限
重写的方法返回值类型不一定和父类的方法相同(可以是协变类型
,返回值构成继承关系)
另外, 针对重写的方法, 可以使用 @Override
注解来显式指定
向上转型是子类对象
转成父类对象
,向下转型就是父类对象
转成子类对象
。相比于向上转型来说,向下转型没那么常见,但是也有一定的用途
要想细究向下转型,我们来看一段代码:
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);
}
}
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 + "正在飞");
}
}
接下来我们在Test里让圆圆吃谷子:
Animal animal = new Bird("圆圆");
animal.eat("谷子");
// 执行结果
圆圆正在吃谷子
接下来我们尝试让圆圆飞起来:
animal.fly();
// 编译出错
找不到 fly 方法
究竟圆圆为啥不能飞呢?
编译过程中, animal 的类型是 Animal,
此时编译器只知道这个类中有一个 eat 方法, 没有 fly 方法
。虽然 animal 实际引用的是一个 Bird 对象, 但是编译器是以 animal 的类型
来查看有哪些方法的。
对于 Animal animal = new Bird("圆圆")
这样的代码,
哪些方法
存在, 看的是 Animal
这个类型父类的方法
还是子类的方法
, 看的是 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(Test.java:35)
请注意,这里不是所有动物都是Bird!animal
本质上引用的是一个 Cat 对象
,是不能转成 Bird 对象的, 运行时就会抛出异常。
所以, 为了让向下转型更安全, 我们可以先判定一下看看 animal 本质上是不是一个 Bird 实例, 再来转换:
Animal animal = new Cat("小猫");
if (animal instanceof Bird) {
Bird bird = (Bird)animal;
bird.fly();
}
instanceof
可以判定一个引用
是否是某个类的实例
。 如果是, 则返回 true. 这时再进行向下转型就比较安全了
前面的代码中由于使用了重写机制, 调用到的是子类的方法。 如果需要在子类内部调用父类方法怎么办? 可以使用super 关键字
,表示对父类实例的引用。
常见用法:
super
和this
的区别:
代表的事物不同:
使用前提不同:
调用事物不同:
这是一段有坑的代码,我们来看一看:
我们创建两个类, B 是父类
,D 是子类
。D 中重写 func 方法
,并且在 B 的构造方法中调用 func
class B {
public B() {
// do nothing
func();
}
public void func() {
System.out.println("B.func()");
}
}
class D extends B {
private int num = 1;
@Override
public void func() {
System.out.println("D.func() " + num);
}
}
public class Test {
public static void main(String[] args) {
D d = new D();
}
}
// 执行结果
D.func() 0
这里调用了子类的方法,说明又发生了动态绑定:
动态绑定
, 会调用到 D 中的 func了解了向上转型
,动态绑定
, 方法重写
之后,我们就可以使用 多态(polypeptide)
的形式来设计程序了。我们可以写一些只关注父类
的代码, 就能够同时兼容各种子类
的情况。
说再多不如上一段代码来理解:
class Shape {
public void draw() {
// 啥都不用干
}
}
class Cycle extends Shape {
@Override
public void draw() {
System.out.println("○");
}
}
class Rect extends Shape {
@Override
public void draw() {
System.out.println("□");
}
}
class Flower extends Shape {
@Override
public void draw() {
System.out.println("♣");
}
}
//---------------------------------------
public class Test {
public static void main(String[] args) {
Shape shape1 = new Flower();
Shape shape2 = new Cycle();
Shape shape3 = new Rect();
drawMap(shape1);
drawMap(shape2);
drawMap(shape3);
}
// 打印单个图形
public static void drawShape(Shape shape) {
shape.draw();
}
}
在这个代码中, 分割线上方的代码是 类的实现者
编写的,分割线下方的代码是 类的调用者
编写的。
当类的调用者在编写 drawMap
这个方法的时候, 参数类型为 Shape (父类)
, 此时在该方法内部并不知道, 也不关注当前的 shape 引用指向的是哪个类型(哪个子类)的实例。
此时 shape 这个引用调用 draw 方法
可能会有多种不同的表现(和 shape 对应的实例相关), 这种行为就称为 多态
我们为什么要使用多态?有什么好处吗?
- 类调用者对类的使用成本进一步降低。
封装是让类的调用者不需要知道类的实现细节
。 多态能让类的调用者连这个类的类型
是什么都不必知道,只需要知道这个对象具有某个方法
即可。
因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低。- 能够降低代码的 “
圈复杂度
”, 避免使用大量的 if - else可扩展能力更强
。如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低