Java核心语法(包、继承、组合、多态、抽象类、接口)
作者:互联网
一、包
1. 概述
包(package)是组织类的一种方式。
使用包的主要目的是保证类的唯一性。
例如,你在代码里写了个Test类,然后你的同事也可能写了个Test类,如果出现两个同名的类,就会起冲突,导致代码不能编译通过。
2. 导入包中的类
Java中已经提供了很多现成的类供我们使用,例如
public class Test {
public static void main(String[] args) {
java.util.Date date = new java.util.Date 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 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());
}
}
在这种情况下需要使用完整的类名
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 **和 using
包下有很多类,但是不会一次性加载进来。使用到谁,就把谁加载进来。
3. 将类放到包中
基本规则
在文件的最上方加上一个
package
语句指定该代码在哪个包中。
包名需要尽量指定成唯一的名字,通常会用公司的域名的颠倒形式(如,com.zzy.demo1)
包名要和代码路径想匹配,例如创建com.zzy.demo1的包,那么会存在一个对应的路径com/zzy/demo1 来存储代码。
如果一个类没有package
语句,则该类被放到一个默认包中。
操作步骤
- 在 IDEA 中先新建一个包:右键 src -> New -> Package
- 在弹出的对话框中输入包名,例如 com.zzy.demo1
- 在包中创建类,右键包名 -> New -> Java class
- 同时我们也看到了,在新创建的 Test.java 文件的最上方,就出现了一个
package
语句
- 此时可以看到磁盘上的目录结构已经被 IDEA 自动创建出来了。
4. 包的访问权限控制
我们已经了解了类中的 public 和 private,private 中的成员只能被类的内部使用。
如果某个成员不包含 public 和 private 关键字,此时这个成员可以在包内部的其他类使用,但是不能在包外部的类使用。
下面的代码给了个示例,Demo1 和 Demo2 是同一个包中,Test 是其他包中
package com.zzy.demo;
public class Demo1 {
int value = 0;
}
package com.zzy.demo;
public class Demo2 {
public static void main(String[] args) {
Demo1 demo = new Demo1();
System.out.println(demo.value);
}
}
import com.zzy.demo1.Demo1;
public class Test {
public static void main(String[] args) {
Demo1 demo = new Demo1();
System.out.println(demo.value);
}
}
5. 常见的系统包
-
java.lang:系统常用基础类(String、Object),此包从JDK1.1后自动导入。
-
java.lang.reflect:java 反射编程包;
-
java.net:进行网络编程开发包。
-
java.sql:进行数据库开发的支持包。
-
java.util:是java提供的工具程序包。(集合类等) 非常重要
-
java.io:I/O编程开发包
二、继承
1. 背景
代码中创建类,主要是为了抽象现实中的一些事物(包含属性和方法)。
有的时候客观事物之间就存在一些关联关系,那么在表示成类和对象的时候也会存在一定的关联。
例如,设计一个类表示动物
注意:我们可以给每个类创建一个单独的 Java 文件,类名必须和 .java 文件名匹配(大小写敏感)
public 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 {
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 这几个类中存在一定的关联关系:
这三个类都具备一个相同的 eat 方法, 而且行为是完全一样的。
这三个类都具备一个相同的 name 属性, 而且意义是完全一样的。
从逻辑上讲, Cat 和 Bird 都是一种 Animal (is - a 语义)。
此时, Animal 这样被继承的类, 我们称为 父类 , 基类 或 超类, 对于像 Cat 和 Bird 这样的类, 我们称为 子类, 派生类 和现实中的儿子继承父亲的财产类似, 子类也会继承父类的字段和方法, 以达到代码重用的效果。
2. 语法规则
基本语法
class 子类 extends 父类 {
}
使用 extends 指定父类.
Java 中一个子类只能继承一个父类 (而C++/Python等语言支持多继承).
子类会继承父类的所有 public 的字段和方法.
对于父类的 private 的字段和方法, 子类中是无法访问的.
子类的实例中, 也包含着父类的实例. 可以使用 super 关键字得到父类实例的引用
对于上面的代码, 可以使用继承进行改进. 此时我们让 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();
}
}
如果我们把 name 改成 private,那么此时子类就不能访问了
class Bird extends Animal {
public Bird(String name) {
super(name);
}
public void fly() {
System.out.println(this.name + "正在飞");
}
}
super 关键字
这个时候我们可以和 this 关键字进行对比:
this:当前对象的引用
- this(); //调用本类其他的构造方法
- this.data; //访问当前类当中属性
- this.func(); //调用本类的其他成员方法
super:代表父类对象的引用
- super(); //调用父类的构造方法,必须放在第一行
- super.data(); // 访问父类的属性
- super.func(); //访问父类的成员方法
class Animal {
public String name;
public Animal(String name) {
this.name = name;
System.out.println("Animal(String)");
}
public void eat(String food) {
System.out.println(this.name + "正在吃" + food);
}
}
class Cat extends Animal {
public Cat(String name) {
// 使用 super 调用父类的构造方法.
super(name); // 必须放在第一行
System.out.println("Cat(String)");
}
从上面的代码可以看到,父类 Animal 有构造方法,所以子类继承父类的时候,也要使用构造方法来构造父类,使用super关键字。
可以看到子类在构造的时候,先帮助父类进行构造。
注意:
- 子类继承了父类除构造方法外的其他所有的。
- 子类在构造的时候,先帮助父类进行构造。
- 如果构造语句中没有写super和this,则默认使用super。
- 如果构造语句中有this,则不能使用super。
public class Test {
public static void main(String[] args) {
B b = new B();
}
}
class A {
A() {
System.out.println("a");
}
A(String name){
System.out.println("a name");
}
}
class B extends A {
B(){
this("abc"); //这里有this了,就不能调用super了
System.out.println("b");
}
B(String name){
// 默认有个super
System.out.println("b name");
}
}
protected 关键字
刚才我们发现,如果把字段设为 private,子类不能访问,但是设成 public,又违背我们“封装”的初衷。
两全其美的办法就是 protected 关键字。
- 对于类的调用者来说,protected 修饰的字段和方法是不能访问的。
- 对于类的 **子类 **和 **同一个包的其他类 **来说,protected 修饰的字段和方法是可以访问的。
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void eat(String food) {
System.out.println(this.name + "正在吃" + food);
}
}
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
}
}
No | 范围 | private | default | protected | public |
---|---|---|---|---|---|
1 | 同一个包中的同一类 | ||||
2 | 同一包中的不同类 | ||||
3 | 不同包中的子类 | ||||
4 | 不同包中的非子类 |
更复杂的继承关系
// Animal.java
public Animal {
...
}
// Cat.java
public Cat extends Animal {
...
}
// ChineseGardenCat.java
public ChineseGardenCat extends Cat {
...
}
// OrangeCat.java
public Orange extends ChineseGardenCat {
...
}
......
如刚才这样的继承方式称为多层继承, 即子类还可以进一步的再派生出新的子类
时刻牢记, 我们写的类是现实事物的抽象. 而我们真正在公司中所遇到的项目往往业务比较复杂, 可能会涉及到一 系列复杂的概念, 都需要我们使用代码来表示, 所以我们真实项目中所写的类也会有很多. 类之间的关系也会更加 复杂.
但是即使如此, 我们并不希望类之间的继承层次太复杂. 一般我们不希望出现超过三层的继承关系. 如果继承层 次太多, 就需要考虑对代码进行重构了. 如果想从语法上进行限制继承, 就可以使用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 修饰的类被继承的时候, 就会编译报错, 此时就可以提示我们这样的继承是有悖这个类设计的初衷的
总结:
final 可以修饰类、属性、方法和局部变量
在某些情况下,程序员有以下需求,就会使用到 final:
- 当不希望类被继承时,可以用final修饰。
- 当不希望父类的某个方法被子类覆盖/重写(override)时,可以用 final 关键字修饰。
**访问修饰符 final 返回类型 方法名**
- 当不希望类的某个属性的值被修改,可以用 final 修饰。
**public final doubel TAX_RATE = 0.08;**
- 当不希望某个局部变量被修改,可以用 final 修饰。
**final double TAX_RATE = 0.08;**
final 使用细节:
- final 修饰的属性又叫常量,一般用 XX_XX_XX 来命名。
- final 修饰的属性在定义时,必须赋初值,并且以后不能再修改,赋值可以在如下位置之一:
- **定义时,如 **
**public final double TAX_RATE = 0.08;**
- 在构造器中
- 在代码块中
- 如果 final 修饰的属性是静态的,则初始化的位置只能是
- 定义时
- 在静态代码块,不能在构造器中赋值
- final 类不能继承,但是可以实例化对象
- 如果类不是 final 类,但是含有 final 方法,则该方法虽然不能重写,但是可以被继承
- 一般来说,如果一个类已经是 final 类了,就没有必要再将方法修饰成 final 方法。
- final 不能修饰构造方法(即构造器)
- final 和 static 往往搭配使用,效率更高,不会导致类加载,底层编译器做了优化处理。
9. 包装类(Integer,Double,Float,Boolean等都是 final),String也是final类
三、组合
和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果. 例如表示一个学校:
public class Student {
...
}
public class Teacher {
...
}
public class School {
public Student[] students;
public Teacher[] teachers;
}
组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段. 这是我们设计类的一种常用方式之一
组合表示 has - a 语义
在刚才的例子中, 我们可以理解成一个学校中 "包含" 若干学生和教师
继承表示 is - a 语义
在上面的 "动物和猫" 的例子中, 我们可以理解成一只猫也 "是" 一种动物
四、多态
1. 向上转型
本质:父类的引用指向了子类的对象
语法:父类类型 引用名 = new 子类类型();
特点:编译类型看左边,运行类型看右边。
- 可以调用父类中的所有成员(需遵守访问权限)
- 不能调用子类中特有成员
- 最终运行效果看子类的具体实现
在刚才的例子中,我们写了形如下面的代码
Bird bird = new Bird("圆圆");
这个代码也可以写成这个样子
Bird bird = new Bird("圆圆");
Animal bird2 = bird;
// 或者写成下面的方式
Animal bird2 = new Bird("圆圆");
此时 bird2 是一个父类 (Animal) 的引用, 指向一个子类 (Bird) 的实例. 这种写法称为 向上转型
为啥叫 "向上转型"?
在面向对象程序设计中, 针对一些复杂的场景(很多类, 很复杂的继承关系), 程序猿会画一种 UML 图的方式来表 示类之间的关系. 此时父类通常画在子类的上方. 所以我们就称为 "向上转型" , 表示往父类的方向转.
class Animal {
public String name;
public Animal(String name) {
this.name = name;
System.out.println("Animal(String)");
}
public void eat(String food) {
System.out.println(this.name + "正在吃" + food);
}
}
class Cat extends Animal {
public int count = 99;
public Cat(String name) {
// 使用 super 调用父类的构造方法.
super(name);
System.out.println("Cat(String)");
}
}
方法传参
public class Test {
//方法传参
public static void func(Animal animal){
animal.eat("food");
}
public static void main(String[] args) {
Cat cat = new Cat("咪咪");
func(cat);
}
}
实参cat传参到形参animal,所以在方法func中,animal的类型是Animal(基类),实际对应到Cat(子类)的实例。
方法返回
public class Test {
//返回值
public static Animal func2() {
// Cat cat = new Cat("咪咪");
// return cat;
//也可以写成下面的形式
return new Cat("咪咪");
}
public static void main(String[] args) {
Animal animal = func2();
animal.eat("nut");
}
}
方法func2返回的是Cat(子类)的引用,animal为(父类)的引用,实际上对应的是Cat的实例。
赋值
public class Test {
public static void main1(String[] args) {
//向上转型-》 父类引用 引用子类对象
Animal animal = new Cat("咪咪");
animal.eat("noodles");
//animal.count; error 向上转型后,通过父类的引用 只能访问父类自己的方法或属性
//父类引用 只能访问自己特有的
}
}
2. 动态绑定
当子类和父类中出现同名方法的时候, 再去调用会出现什么情况呢? 对前面的代码稍加修改, 给 Bird 类也加上同名的 eat 方法, 并且在两个 eat 中分别加上不同的日志
// 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 class Test {
public static void main(String[] args) {
Animal animal1 = new Animal("圆圆");
animal1.eat("谷子");
Animal animal2 = new Bird("扁扁");
animal2.eat("谷子");
}
}
// 执行结果
我是一只小动物
圆圆正在吃谷子
我是一只小鸟
扁扁正在吃谷子
此时, 我们发现:
animal1 和 animal2 虽然都是** Animal** 类型的引用, 但是 animal1 指向 **Animal **类型的实例, animal2 指向 Bird 类型的实例.
针对 animal1 和 animal2 分别调用 eat 方法, 发现 **animal1.eat() **实际调用了父类的方法, 而 animal2.eat() 实际调用了子类的方法
因此, 在 Java 中, 调用某个类的方法, 究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) , 要看究竟这个引用指向的是父类对象还是子类对象. 这个过程是程序运行时决定的(而不是编译期), 因此称为 动态绑定(或者是运行时绑定)。注意一点,属性没有动态绑定机制,哪里声明,哪里使用。
> **运行时绑定:** > **父类引用 引用子类对象。同时通过父类引用调用同名的覆盖方法,此时就会发生运行时绑定。**
3. 方法重写
针对刚才的 eat 方法来说: 子类实现父类的同名方法, 并且参数的类型和个数完全相同, 这种情况称为 覆写/重写/覆盖(Override).
> **重写:override** > 1. **方法名称相同** > 1. **返回值相同** > 1. **参数列表相同** > 1. **不同的类 -》 继承关系上。**
> **关于重写的注意事项:** > 1. 重写和重载完全不一样。 > 1. 普通方法可以重写,static 修饰的静态方法不能重写。 > 1. 重写中子类的方法的访问权限不能低于父类的方法的访问权限。(private < default < protected < public) > 1. 重写的方法返回值类型不一定和父类的方法相同(但是建议最好写成相同,特殊情况除外) > 1. 需要重写的方法,不能是被 final 修饰的。被 final 修饰之后,他是密封的方法,不可以修改。
方法权限示例:将子类的 eat 方法改为 private
// Animal.java
public class Animal {
public void eat(String food) {
...
}
}
// Bird.java
public class Bird extends Animal {
// 将子类的 eat 改成 private
private void eat(String food) {
...
}
}
// 编译出错
//Error:(8, 10) java: com.bit.Bird中的eat(java.lang.String)无法覆盖com.zzy.Animal中的
//eat(java.lang.String)
// 正在尝试分配更低的访问权限; 以前为public
另外,针对重写的方法,可以使用 @Override 注解来显示指定
// Bird.java
public class Bird extends Animal {
@Override
private void eat(String food) {
...
}
}
有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发 现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写. 我们推荐在代码中进行重写方法时显式加上 @Override 注解
小结:
事实上, 方法重写是 Java 语法层次上的规则, 而动态绑定是方法重写这个语法规则的底层实现. 两者本质上描述的是相同的事情, 只是侧重点不同。
4. 向下转型
向上转型是子类对象转成父类对象, 向下转型就是父类对象转成子类对象. 相比于向上转型来说, 向下转型没那么常见, 但是也有一定的用途
// 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 方法
注意事项:
编译过程中,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();
animal 本质上引用的是一个 Cat 对象,是不能转换成 Bird 对象的,运行时就会抛出异常。
所以,为了让向下转型更安全,我们可以先判定一下看看 animal 本质上是不是一个 Bird 实例,再来转换
Animal animal = new Cat("八哥");
// A instanceof B 判断A 是不是 B的一个实例
if (animal instanceof Bird) {
//向下转型
Bird bird = (Bird)animal;
bird.fly();
} else {
System.out.println("sadadsadsas");
}
**instanceof**
,比较操作符,用于判断对象的运行类型是否为XX类型或XX的子类型。如果是,则返回true,这时再进行向下转型就比较安全。
5. 在构造方法中调用重写的方法(一个坑)
一段有坑的代码,我们创建两个类,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
结论:
"用尽量简单的方式使对象进入可工作状态", 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发 动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题
6. 理解多态
- 什么是多态?
- 父类引用 引用子类对象
- 父类和子类有同名的覆盖方法
- 通过父类引用代用这个重写的方法的时候。
class Shape {
public void draw() {
}
}
class Cycle extends Shape {
@Override
public void draw(){
System.out.println("画一个○");
}
}
class React extends Shape {
@Override
public void draw() {
System.out.println("画一个矩形");
}
}
////////////////////////分割线///////////////////////
public class Demo1 {
public static void drawMap(Shape shape) {
shape.draw();
}
public static void main(String[] args) {
Shape shape1 = new Cycle();
Shape shape2 = new React();
drawMap(shape1);
drawMap(shape2);
}
}
在这个代码中,分割线上方的代码是 **类的实现者 **编写的,分割线下方的代码是 **类的调用者 **编写的。
当类的调用者在编写 drawMap
这个方法的时候,参数类型为 Shape(父类),此时在该方法内部并不知道,也不关注当前的 shape 引用指向的是哪个类型(哪个子类)的实例。此时 shape 这个引用调用 draw 方法可能会有多种不同的表现(和 shape 对应的实例相关),这种行为称为 多态。
使用多态的好处是什么?
- 类调用者对类的使用成本进一步降低
- 封装是让类的调用者不需要指定类的实现细节
- 多态能让类的调用者连这个类的类型是什么都不必知道,只需要知道这个对象具有某个方法即可。
因此,多态可以理解成是封装的更进一步,让类的调用者对类的使用成本进一步降低。
- 能够降低代码的“圈复杂度”,避免使用大量的 if - else
public static void drawShapes() {
React rect = new React();
Cycle cycle = new Cycle();
Triangle triangle = new Triangle();
String[] shapes = {"cycle", "rect", "cycle", "rect", "triangle"};
for(String shape : shapes) {
if(shape.equals("cycle")) {
cycle.draw();
} else if (shape.equals("rect")) {
rect.draw();
} else if (shape.equals("triangle")) {
triangle.draw();
}
}
}
如果使用多态,则不必写这么多的 if - else 分支语句,代码更简单。
public static void drawShapes() {
// 我们创建了一个 Shape 对象的数组.
Shape[] shapes = {new Cycle(), new Rect(), new Cycle(),
new Rect(), new Triangle()};
for (Shape shape : shapes) {
shape.draw();
}
}
什么是“圈复杂度”?
圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂.因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 "圈复杂度". 如果一 个方法的圈复杂度太高, 就需要考虑重构
- 可扩展能力更强
如果要新增一种新的形状,使用多态的方法改动代码成本比较低
class Flower extends Shape {
@Override
public void draw() {
System.out.println("Flower!");
}
}
对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低. 而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高
五、抽象类
1. 语法规则
在刚才的打印图形例子中, 我们发现, 父类 Shape 中的 draw 方法好像并没有什么实际工作, 主要的绘制图形都是由 Shape 的各种子类的 draw 方法来完成的. 像这种没有实际工作的方法, 我们可以把它设计成一个 抽象方法(abstract method), 包含抽象方法的类我们称为 抽象类(abstract class)
abstract class Shape {
abstract public void draw();
}
在 draw 方法前加上 abstract 关键字, 表示这是一个抽象方法. 同时抽象方法没有方法体(没有 { }, 不能执行具体 代码).
对于包含抽象方法的类, 必须加上 abstract 关键字表示这是一个抽象类
注意事项:
- 抽象类不能直接实例化
Shape shape = new Shape();
// 编译出错
//Error:(30, 23) java: Shape是抽象的; 无法实例化
- 抽象方法不能是 private 的
abstract class Shape {
abstract private void draw();
}
// 编译出错
//Error:(4, 27) java: 非法的修饰符组合: abstract和private
- 抽象类中可以包含其他的非抽象方法, 也可以包含字段. 这个非抽象方法和普通方法的规则都是一样的, 可以被重写, 也可以被子类直接调用
abstract class Shape {
abstract public void draw();
void func() {
System.out.println("func");
}
}
class Rect extends Shape {
...
}
public class Test {
public static void main(String[] args) {
Shape shape = new Rect();
shape.func();
}
}
// 执行结果
//func
2. 抽象类的作用
抽象类存在的最大意义就是为了被继承.
抽象类本身不能被实例化, 要想使用, 只能创建该抽象类的子类. 然后让子类重写抽象类中的抽象方法.
小结:
- 抽象类不能被实例化。
- 抽象类不一定要包含abstract方法。也就是说抽象类可以没有abstract方法。
- 一旦类包含了abstract方法,则这个类必须声明为abstract。
- abstract 只能修饰类和方法,不能修饰属性和其他。
- 抽象类可以有任意成员(抽象类本质还是类),比如:非抽象方法、构造器、静态属性等。
- 抽象方法不能有主体,即不能实现。
- 如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法,除非它自己也声明成abstract类。
- 抽象方法不能使用 private、final 和 static来修饰,因为这些关键字都是和重写相违背的。
六、接口
接口是抽象类的更进一步. 抽象类中还可以包含非抽象方法和字段. 而接口中包含的方法都是抽象方法, 字段只能包含 静态常量.
1. 语法规则
在刚才的打印图形的示例中, 我们的父类 Shape 并没有包含别的非抽象方法, 也可以设计成一个接口
interface IShape {
void draw();
}
class Cycle implements IShape {
@Override
public void draw() {
System.out.println("○");
}
}
public class Test {
public static void main(String[] args) {
IShape shape = new Rect();
shape.draw();
}
}
使用 interface 定义一个接口
接口中的方法一定是抽象方法, 因此可以省略 abstract
接口中的方法一定是 public, 因此可以省略 public
Cycle 使用 implements 继承接口. 此时表达的含义不再是 "扩展", 而是 "实现"
在调用的时候同样可以创建一个接口的引用, 对应到一个子类的实例.
接口不能单独被实例化
扩展(extends) vs 实现(implements)
扩展指的是当前已经有一定的功能了,进一步扩充功能。
实现指的是当前啥都没有,需要从头构造出来。
接口中只能包含抽象方法. 对于字段来说 , 接口中只能包含静态常量(final static).
interface IShape {
void draw();
public static final int num = 10;
}
其中的 public, static, final 的关键字都可以省略. 省略后的 num 仍然表示 public 的静态常量.
我们创建接口的时候, 接口的命名一般以大写字母 I 开头.
接口的命名一般使用 "动词" 词性的单词.
阿里编码规范中约定, 接口中的方法和属性不要加任何修饰符号, 保持代码的简洁性
2. 实现多个接口
有的时候我们需要让一个类同时继承自多个父类. 这件事情在有些编程语言通过 多继承 的方式来实现的. 然而 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 + "正在用尾巴游泳");
}
}
class Frog extends Animal implements IRunning, ISwimming {
public Frog(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.name + "正在往前跳");
}
@Override
public void swim() {
System.out.println(this.name + "正在蹬腿游泳");
}
}
上面的代码展示了 Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口
继承表达的含义是:
is - a
语义,而接口表达的含义是具有 ××× 特性
。
如,猫具有会跑的特性。
这样设计有什么好处呢? 时刻牢记多态的好处, 让程序猿忘记类型. 有了接口之后, 类的使用者就不必关注具体类型, 而 只关注某个类是否具备某种能力.
3. 接口使用实例
给对象数组排序
给定一个学生类
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),
};
让我们的 Student 类实现 Comparable 接口, 并实现其中的 compareTo 方法
class Student implements Comparable<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 + "]";
}
@Override
public int compareTo(Student o){
if(this.score > s.score){
return -1;
}else if (this.score < s.score){
return 1;
}else{
return 0;
}
}
}
public static void main(String[] args) {
Student student1 = new Student("张三", 95),
Student student2 = new Student("李四", 96);
Student student3 = new Student("王五", 97);
Student[] students = new Student[3];
students[0] = student1;
students[1] = student2;
students[2] = student3;
Arrays.sort(students);
System.out.println(Arrays.toString(students));
}
在 sort 方法中会自动调用 compareTo 方法. compareTo 的参数传入的是 Student 类型的对象. 然后比较当前对象和参数对象的大小关系(按分数来算)
如果当前对象应排在参数对象之前, 返回小于 0 的数字;
如果当前对象应排在参数对象之后, 返回大于 0 的数字;
如果当前对象和参数对象不分先后, 返回 0
// 执行结果
[[王五:97], [李四:96], [张三:95]]
小结:
- 接口不能被实例化。
- 接口中所有的方法是 public 方法,接口中抽象方法,可以不用 abstract 修饰。
- 一个普通类实现接口,就必须将该接口的所有方法都实现。
- 抽象类实现接口,可以不用实现接口的方法。
- 一个类同时可以实现多个接口
**class A implements IB,IC{}**
- 接口中的属性只能是 final 的,而且是 public static final 修饰符。比如:int a = 1,实际上是 public static final int a = 1;(必须初始化)
- 接口中属性的访问形式:接口名.属性名
- 接口不能继承其他的类,但是可以继承多个别的接口
**interface A extends B,C{}**
- **接口的修饰符只能是 public 和 默认,这点和类的修饰符是一样的。 **
4. Cloneable 接口和深拷贝
Java 中内置了一些很有用的接口,Cloneable 就是其中之一。
Object 类中存在一个 clone 方法,调用这个方法可以创建一个对象的“拷贝”。但是想要合法调用clone方法,必须要先实现 Cloneable 接口,否则就会抛出 CloneNotSupportException 异常。
注意:
对于内置类型,clone方法所涉及的是深拷贝。
对于自定义类型,clone方法所涉及的是浅拷贝,需要做些处理。
class Person implements Cloneable{
public int age;
@Override //必须重写clone方法
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Person person1 = new Person();
Person person2 = (Person)person1.clone();
System.out.println(person1.age);
System.out.println(person2.age);
System.out.println("===========修改后==============");
person2.age = 100;
System.out.println(person1.age);
System.out.println(person2.age);
}
}
但是如果类里面的变量又引用了其他的自定义类,则需要做出下面的修改:
class Money implements Cloneable{
public double money = 12.5;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Person implements Cloneable{
public int age;
Money m = new Money(); //类中又引用了其他的自定义类
@Override
protected Object clone() throws CloneNotSupportedException {
//return super.clone();
//1.克隆person
Person p = (Person)super.clone(); //做的是复制下图的黄框
//2.克隆当前的Money对象
p.m = (Money)this.m.clone(); //做的事复制下图的红框
return p;
}
}
public class Test {
public static void main3(String[] args) throws CloneNotSupportedException {
Person person1 = new Person();
Person person2 = (Person)person1.clone();
System.out.println(person1.m.money);
System.out.println(person2.m.money);
System.out.println("===========修改后==============");
person2.m.money = 99.9;
System.out.println(person1.m.money);
System.out.println(person2.m.money);
}
}
通过clone拷贝出的 person 对象只是拷贝了 person 自身,而没有拷贝内部包含的 m 对象,此时 person1 和 person2 中包含的 m 引用仍然是指向同一个对象,此时修改一遍,另一边也会发生变化。
- 总结:想克隆自定义类型的步骤
- 实现接口
public interface Cloneable{
}
这时,我们就有个疑惑了,为啥是个空接口?
这是一个面试问题
空接口:也把它叫做标记接口,意思是只要一个类实现了这个接口,那么就标记这个类,是可以克隆的。
在 IDEA 中,我们可以进入Cloneable:
- 重写clone方法,即对Object类中clone方法的重写。
5. 总结
**核心区别: **抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写), 而接口中不 能包含普通方法, 子类必须重写所有的抽象方法.
标签:Java,String,void,多态,class,Animal,抽象类,public,name 来源: https://www.cnblogs.com/zhenyucode/p/16438429.html