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