面向对象编程
面向对象
面向对象的三大基本特征: 封装,继承,多态
面向对象三大核心特性:可重用,可拓展,可管理。
面向对象就是将我们的事务分析其特征作为对象属性,行为作为对象的方法。
包
包用于存放功能相同或者相似的类。主要目的是为了保证类的唯一性。
导包
示例:导入Date类:java.util.Date;
import java.util.Date;
要导入util中的其他类就使用.*
import java.util.*;
注意:
import导入的无论如何都是类,而不能理解成导入了一个包!这里的*号指的是根据实际情况调用需要的类。如果我们在使用类的时候在当前的类里面导入的包里面有重名的类,要使用正确的类就在使用的时候写完整的类名加上路径。
比如:Date在java.sql中有,java.util中也有:
import java.sql.*;
import java.util.*;
public class DateTest{
public static void main(String [] args){
java.util.Date date = new java.util.Date();
}
}
使用import static 可以导入包中的静态方法和字段。提高开发效率。
import static java.lang.System.*;
public class SystemTest{
public static void mian(String [] args){
out.println("hello");
}
}
常见的系统开发包
1.java.lang: 系统常用基础类(String,Object)。该包从JDK1.1后自动导入。
2.java.lang.reflect: java反射编程包
3.java.net: 进行网络编程开发包
4.java.sql: 进行数据库开发的支持包
5.java.util:是java提供的工具程序包。(集合类等)非常重要。
6.java.io: I/O编程开发包
继承
像现实生活中的一些事务属于另一个事务的一类的。比如狸花猫属于猫,猫属于动物,狸花猫有猫的行为和特点,像这样的就是狸花猫继承了猫。
public class Cat{
private int leg =4;
public void walk(){
System.out.println("i can walk");
}
}
public class LiHuaCat extends Cat{
public void GetName(){
System.out.println("我是狸花猫");
}
}
注意:
1.使用 extends 指定父类.
2.Java 中一个子类只能继承一个父类 (而C++/Python等语言支持多继承).
3.子类会继承父类的所有 public 的字段和方法.
4.对于父类的 private 的字段和方法, 子类中是无法访问的(但其实是隐式继承了,只是没办法调用).
5.子类的实例中, 也包含着父类的实例. 可以使用 super 关键字得到父类实例的引用。
6.new一个子类的时候,会先调用父类的构造块和构造方法。(所以在类加载的时候也会先加载父类,调用父类的静态代码块,然后才到子类的静态代码块)
7.Java虽然只能单继承,但允许多层继承。
Super关键字
1.super先从直接父类中寻找同名属性,若不存在再向上寻找。
2.this直接从当前类中寻找同名属性,若不存在再向上搜索。
3.若父类中不存在无参构造,则子类构造方法的首行必须使用super(有参构造)
4.在一个构造方法中无法显式使用this()和super()同时出现。
public class ObjectTest {
public class Cat extends Anmail{
public Cat(){
super();
}
}
public class Anmail{
public void move(){
System.out.println("i can move");
}
}
}
注意:
1.super不能指代当前父类的引用
2.当父类中只有有参构造方法时,子类构造方法的首行必须显式使用super调用父类这个有参构造,否则会报错。(重要)
多态
一个引用可以表现出多种行为/特性
向上转型
1.向上转型发生在有继承的类之间
2.父类名称 父类引用=new 子类实例();
public class Animal {
private String name;
public void move(){
System.out.println("i can move");
}
public void eat(String food){
System.out.println(this.name+"正在吃:"+food);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Animal(String name){
this.name=name;
}
public Animal(){
}
}
public class Bird extends Animal {
private int leg;
public void fly(){
System.out.println("能飞");
}
public Bird(String name){
super(name);
}
}
public class ObjectTest {
public static void main(String[] args) {
Bird bird = new Bird("小鸟");
Bird bird1 = new Bird("小鸟2");
Animal animal1 = bird1;
Animal bird3 = new Bird("小鸟3");
}
}
向上转型发生的场景
方法传参
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 的实例。
向上转型的意义
此时方法 findMyAnimal 返回的是一个 Animal 类型的引用, 但是实际上对应到 Bird 的实例。
动态绑定
在 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("谷子");
}
}
// 执行结果
我是一只小动物
圆圆正在吃谷子
我是一只小鸟
扁扁正在吃谷子
方法重写
方法重载:同一个类中,定义了若干个方法名称相同,参数列表不同的一组方法。
方法重写:有继承关系的类之间,子类定义了和父类除了权限不同,其他全都相同的方法,这样一组方法称为方法重写。
注意事项:
重写和重载完全不一样. 不要混淆(思考一下, 重载的规则是啥?)
普通方法可以重写, static 修饰的静态方法不能重写
重写中子类的方法的访问权限不能低于父类的方法访问权限.
重写的方法返回值类型不一定和父类的方法相同(但是建议最好写成相同, 特殊情况除外)
这里的特殊情况指的是,可以定义父类的返回值类型,但return了一个子类的类型。 针对重写的方法, 可以使用 @Override 注解来显式指定
public clas Animal{
public void eat(){
System.out.println("Animal eat food");
}
}
public class Bruid extends Animal{
@Override
public void eat(){
System.out.println("Bruid eat food");
}
}
多态的意义
- 类调用者对类的使用成本进一步降低
封装是让类的调用者不需要知道类的实现细节.
多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可.
因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低.
这也贴合了 <<代码大全>> 中关于 “管理代码复杂程度” 的初衷. - 能够降低代码的 “圈复杂度”, 避免使用大量的 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 进行一定的修改, 改动成本更高。
向下转型
向上转型是子类对象转成父类对象, 向下转型就是父类对象转成子类对象. 相比于向上转型来说, 向下转型没那么常见,但是也有一定的用途
// 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. 这时再进行向下转型就比较安全
这里有一个坑:
一段有坑的代码. 我们创建两个类, 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 对象的同时, 会调用 B 的构造方法
B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的 func()
此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0.
结论: “用尽量简单的方式使对象进入可工作状态”, 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题。
抽象类
没有实际工作的方法, 我们可以把它设计成一个抽象方法(abstract method),包含抽象方法的类我们称为 抽象类(abstract class)。
public abstract class Shape{
public abstract void drae();
}
注意事项:
1.抽象方法是没有方法体的
2.含抽象方法的类必须加上abstract修饰。
3.抽象类不能实例化
4.抽象方法不能是private,不然就没有子类了。
5.抽象类不仅包含抽象方法,也可以包含非抽象。
6.抽象方法所在的类必须是抽象类,子类若继承了抽象类,必须覆写所有的抽象方法(子类是普通类的情况下),抽象类也可以被抽象类继承,此时可以留给这个抽象类的子类去覆写。
7.Java中没有方法体的方法不仅仅只有抽象方法。
8.抽象类是普通类的集合,虽然没法直接俄实例化对象,但是也可以存在构造方法。并且也遵循继承原则。
抽象类的作用
1.为了被继承
2.多了一层编译器的校验。
3.充分利用编译器的校验,我们利用abstract以及final等这些进行了规定后,一旦编写代码我们失误了,编译器会及时提醒我们。
接口
如果一个类只有抽象方法,那么就可以设计成接口。
interface IAnimal{
void eat();
}
class Cat implements IAnimal{
@Override
public void eat(){
System.out.println("猫吃鱼");
}
}
语法规则
1.使用interface定义了一个接口。
2.接口中的方法一定是抽象方法,所以可以省略abstract.
3.接口的方法一定是public,所以就可以省略public.
4.使用implements继承了接口,就表示不是拓展,而是实现接口。
5.在调用的时候同样可以创建一个接口的引用, 对应到一个子类的实例.
6.接口不能单独被实例化.
7.接口允许多实现
8.扩展(extends) vs 实现(implements)
扩展指的是当前已经有一定的功能了, 进一步扩充 功能.
实现指的是当前啥都没有, 需要从头构造出来
9.接口中只能包含抽象方法. 对于字段来说, 接口中只能包含静态常量(final static).
interface IAnimal{
void eat();
public static final int NUM = 1;
}
public, static, final 的关键字都可以省略. 省略后的 num 仍然表示 public 的静态常量
interface IAnimal{
void eat();
int NUM = 1;
}
注意事项
1.我们创建接口的时候, 接口的命名一般以大写字母 I 开头.
2.接口的命名一般使用 “形容词” 词性的单词.
3.阿里编码规范中约定, 接口中的方法和属性不要加任何修饰符号, 保持代码的简洁性,只保留方法返回值,方法参数列表,名称即可
子类实现一个接口的时候,命名以相对应的接口开头,以impl结尾。
eg:如果是IRun的子类,则建议命名为RunImpl(也不是强制要求)
4.如果子类实现多个父接口,不需要使用此规范命名。
5.IEA中使用CTRL+快速实现接口
实现多个接口
Java 中只支持单继承, 一个类只能 extends 一个父类. 但是可以同时实现多个接口, 也能达到多继承类似的效果.一个类继承一个父类, 同时实现多种接口.
class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
}
interface IFlying {
void fly();
}
interface IRunning {
void run();
}
interface ISwimming {
void swim();
}
class Cat extends Animal implements IRunning {
public Cat(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.name + "正在用四条腿跑");
}
}
class Fish extends Animal implements ISwimming {
public Fish(String name) {
super(name);
}
@Override
public void swim() {
System.out.println(this.name + "正在用尾巴游泳");
}
}
Object类
Java中有所有的类名都有一个父类:Object类,不用使用extends来定义,实现参数最高统一化。
注意:
1.所有的类型都可以向上转型成Object类型。
2.Object类中的所有方法都被子类继承下来了。
3.Java中检测数据类型相等使用equals方法比较双方的地址。
4.Object类可以接受所有引用数据类型的对象(接口、数组、类),因此在Java中,若一个方法参数或者返回值是Object类型,说明该参数或者返回值可以是任意引用数据类型(数组、类、接口),除了8大基本类型没法用Object类来接收之外,所有类型都能使用Object类来接收。包装类应运而生。
5.如果自己设计的类后面业务要比较属性的方法就要复写equals方法。
@Override
public boolean equals(Object obj) {
// 1.若当前对象就是obj
if (this == obj) {
return true;
}
// 2.此时当前对象和obj指向的对象确实不是一个地址
// 若此时obj指向的对象和当前类压根没关系,obj指向一个非当前类的对象,没有可比性,直接返回false
if (obj instanceof Animal) {
// 3.obj这个引用指向的对象确实是当前类的对象且和当前对象不是一个对象
// Object obj = new Animal();
Animal ani = (Animal) obj;
// 所有引用类型比较属性值一定要用equals方法,"=="比的是地址!!!!!!!!
return this.name == ani.name&& this.name.equals(ani.name);
}
return false;
}
给对象数组排序
给定一个学生类
class Student {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "[" + this.name + ":" + this.score + "]";
}
}
再给定一个学生对象数组, 对这个对象数组中的元素进行排序(按分数降序).
Student[] students = new Student[] {
new Student("张三", 95),
new Student("李四", 96),
new Student("王五", 97),
new Student("赵六", 92),
}
按照我们之前的理解, 数组工具类我们有一个现成的 sort 方法, 能否直接使用这个方法呢?
Arrays.sort(students);
System.out.println(Arrays.toString(students));
// 运行出错, 抛出异常.
Exception in thread "main" java.lang.ClassCastException: Student cannot be cast to java.lang.Comparable
仔细思考, 不难发现, 和普通的整数不一样, 两个整数是可以直接比较的, 大小关系明确. 而两个学生对象的大小关系怎么确定? 需要我们额外指定!
让我们的 Student 类实现 Comparable 接口, 并实现其中的 compareTo 方法:
class Student implements Comparable {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "[" + this.name + ":" + this.score + "]";
}
@Override
public int compareTo(Object o) {
if(this == 0){
return 0; //返回0表示相等
}
if(o instanceof Student){
//当前传入的o就是Student类型的引用,向下转型还原为Student
//要比较Student对象的大小关系,就要用到Student的独有属性,向下转型
Student stu = (Student)o;
return this.score - stu.score;
}
//若传入不是学生类,则抛出异常
throw new IllegalArgumentException("不是学生类型,无法比较!")
}
在 sort 方法中会自动调用 compareTo 方法. compareTo 的参数是 Object , 其实传入的就是 Student 类型的对象.
然后比较当前对象和参数对象的大小关系(按分数来算).
1.如果当前对象应排在参数对象之前, 返回小于 0 的数字;
2.如果当前对象应排在参数对象之后, 返回大于 0 的数字;
3.如果当前对象和参数对象不分先后, 返回 0;
再次执行程序, 结果就符合预期了
// 执行结果
[[王五:97], [李四:96], [张三:95], [赵六:92]]
注意事项:
对于 sort 方法来说, 需要传入的数组的每个对象都是 “可比较” 的, 需要具备 compareTo 这样的能力. 通过重写 compareTo 方法的方式, 就可以定义比较规则。
为了进一步加深对接口的理解, 我们可以尝试自己实现一个 sort 方法来完成刚才的排序过程(使用冒泡排序):(其实Arrays.sort()内部也是和下面代码类似的,只是被封装了)
public static void sort(Comparable[] array) {
for (int bound = 0; bound < array.length; bound++) {
for (int cur = array.length - 1; cur > bound; cur--) {
if (array[cur - 1].compareTo(array[cur]) > 0) {
// 说明顺序不符合要求, 交换两个变量的位置
Comparable tmp = array[cur - 1];
array[cur - 1] = array[cur];
array[cur] = tmp;
}
}
}
}
再次执行代码
sort(students);
System.out.println(Arrays.toString(students));
// 执行结果
[[王五:97], [李四:96], [张三:95], [赵六:92]]
Clonable 接口
类似于Clonable接口,把这种接口称之为“标记接口”,这种接口本身内部没有任何的抽象方法,只有打上这个标记的子类才拥有克隆能力。(类似于Clonable接口,把这种接口称之为“标记接口”,这种接口本身内部没有任何的抽象方法,只有打上这个标记的子类才拥有克隆能力,clone()是Object类提供的方法)。
注意:
Object 类中存在一个 clone 方法, 调用这个方法可以创建一个对象的 “拷贝”. 但是要想合法调用 clone 方法, 必须要先实现 Clonable 接口, 否则就会抛出 CloneNotSupportedException 异常。
class Animal implements Cloneable {
private String name;
@Override
public Animal clone() {
Animal o = null;
try {
o = (Animal)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return o;
}
}
public class Test {
public static void main(String[] args) {
Animal animal = new Animal();
Animal animal2 = animal.clone();
System.out.println(animal == animal2);
}
}
// 输出结果
// false
深拷贝VS浅拷贝
Cloneable 拷贝出的对象是一份 "浅拷贝”
public class Test {
static class A implements Cloneable {
public int num = 0;
@Override
public A clone() throws CloneNotSupportedException {
return (A)super.clone();
}
}
static class B implements Cloneable {
public A a = new A();
@Override
public B clone() throws CloneNotSupportedException {
return (B)super.clone();
}
}
public static void main(String[] args) throws CloneNotSupportedException {
B b = new B();
B b2 = b.clone();
b.a.num = 10;
System.out.println(b2.a.num);
}
}
// 执行结果
10
注意:
1.通过 clone 拷贝出的 b 对象只是拷贝了 b 自身, 而没有拷贝内部包含的 a 对象. 此时 b 和 b2 中包含的 a 引用仍然是指向同一个对象. 此时修改一边, 另一边也会发生改变。
2.Java中实现深拷贝的方法有两种:
递归使用clone()方法
序列化(json字符串)
参考文献
文献地址:{https://developer.aliyun.com/article/1054031?spm=a2c6h.24874632.expert-profile.42.5f674f97RWUY6P}