前言
包
导入包中的类
静态导入
将类放到包中
包的访问权限控制
常见的系统包
继承
语法规则
protected关键字
更为复杂的继承关系
final关键字
组合
多态
向上转型
动态绑定
方法重写
向下转型
理解多态
super关键字
抽象类
抽象类的作用
总结
接口
语法规则
实现多个接口
总结
前言
这篇博客的目的就是帮助我们进入面向对象编程的核心部分,是非常重要的语法部分。我们常常将面向对象这种编程思想挂在嘴边,却不知道到底什么是面向对象,希望这篇博客能够给你帮助。
包
包 (package) 是组织类的一种方式。使用包的主要目的是保证类的唯一性。什么意思呢?例如, 你在代码中写了一个 Test 类. 然后你的同事也可能写一个 Test 类. 如果出现两个同名的类, 就会冲突, 导致代码不能编译通过。但是引入包的概念就不会发生这种情况,因为在包外同名不会相互影响。就像一个文件夹中不能有同名文件一样。
导入包中的类
Java 中已经提供了很多现成的类供我们使用。 例如:
import java.util.Date;
public class Test {
public static void main(String[] args) {
Date date = new Date();
// 得到一个毫秒级别的时间戳
System.out.println(date.getTime());
}
}
当我们使用类Data时,我们就需要导入包中的类Data,否则无法通过编译,import作用就是在当前类中导入某个包中类,以便当前类引用包中的类。
如果需要使用 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.*;
public class Test {
public static void main(String[] args) {
Date date = new Date();
// 得到一个毫秒级别的时间戳
System.out.println(date.getTime());
int[] array = {1, 2, 3, 4, 5};
System.out.println(Arrays.toString(array));
}
}
Arrays和Data类都在包util中,因此当我们同时用到Arrays和Data类时,直接使用.*就能导入相关的类,.*的好处就体现了出来,当然导入包中类是使用了哪个类才会导入,不是同的类不会导入,这就和C语言明显的不同,C语言使用include导入是将所有的代码都导入,而.*就是将需要使用的类导入到当前类,那么我们以后都用这个导入不就很省事了嘛?其实并不是这样的。还会出现另一种情况:
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());
}
}
1 2 3 4 5 6 7 8 9
在这种情况下就会出错,为什么呢?因为在util包中有Data类,而sql包中也有Data类。这样编译器就不能分辨程序员到底是要哪个类,因此我们需要指明这里是哪一个包中的Data类:
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 3 4 5 6 7 8
静态导入
使用 import static 可以导入包中的静态的方法和字段。
import static java.lang.System.*;
public class Test {
public static void main(String[] args) {
out.println(“hello”);
}
}
使用这种方式可以更方便的写一些代码, 例如:
mport 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);
}
}
静态导入包还是比较少见的,主要目的就是方便写一些代码,但是一些人并不能了解,因此建议新手了解就可以了,不建议采用这些方法编写代码。
将类放到包中
基本规则:
在文件的最上方加上一个 package 语句指定该代码在哪个包中。 包名需要尽量指定成唯一的名字, 通常会用公司的域名的颠倒形式(例如 com.bit.demo1 )。 包名要和代码路径相匹配. 例如创建 com.bit.demo1 的包, 那么会存在一个对应的路径 com/bit/demo1 来存储代码。 如果一个类没有 package 语句, 则该类被放到一个默认包中。
操作步骤:
(1)在 IDEA 中先新建一个包: 右键 src -> 新建 -> 包
在这里插入图片描述
(2)在弹出的对话框中输入包名, 例如 com.bit.demo1。
在这里插入图片描述
(3)在包中创建类, 右键包名 -> 新建 -> 类, 然后输入类名即可。
在这里插入图片描述
(4)此时可以看到我们的磁盘上的目录结构已经被 IDEA 自动创建出来了。
在这里插入图片描述
(5)) 同时我们也看到了, 在新创建的 Test.java 文件的最上方, 就出现了一个 package 语句
在这里插入图片描述
包的访问权限控制
我们已经了解了类中的 public 和 private. private 中的成员只能被类的内部使用。
如果某个成员不包含 public 和 private 关键字, 此时这个成员可以在包内部的其他类使用, 但是不能在包外部的类使用。举个例子:
Test.java
package com.bit.demo1;
public class Test {
int val = 10;//默认包访问权限
}
TestDemo.java
package com.bit.demo1;
public class TestDemo {
public static void main(String[] args) {
Test test = new Test();
System.out.println(test.val);
}
}
我们可以看到这两个类在同一个包里面而在Test类中,没有访问权限修饰符。因此这时是默认包访问权限,因此打印结果为10。
但是如果在非本包类中使用Test类中的val,就会报错。
import com.bit.demo1.Test;
public class TestDemo {
public static void main(String[] args) {
Test test =new Test();
System.out.println(test.val);
}
在这里插入图片描述
常见的系统包
java.lang:系统常用基础类(String、Object),此包从JDK1.1后自动导入。 java.lang.reflect:java 反射编程包; java.net:进行网络编程开发包。 java.sql:进行数据库开发的支持包。 java.util:是java提供的工具程序包。(集合类等) 非常重要 java.io:I/O编程开发包。
继承
我们在前面博客已经介绍过了面向对象的基本特征——封装,封装是不必要公开的数据成员和方法,使用private关键字进行修饰。封装的意义就在于提高安全性。接下来我们介绍第二个基本特性——继承。我们先来看这样一个例子:
class Dog {
public String name;
public int age;
public void eat() {
System.out.println("吃");
}
}
class Bird {
public String name;
public int age;
public String wing;
public void eat(){
System.out.println(“吃”);
}
public void fly(){
System.out.println(“飞”);
}
}
我们定义两个类Bird和Dog类,这两个类有一些共性的地方也就是name,age,eat()方法。如果这样写,会产生大量冗余的代码。因此我们可以将共性的代码抽象出来,写成一个类Animal,然后用Bird和Dog这两个类继承Animal类就可以解决冗余代码的问题。
class Animal {
public String name;
public int age;
public void eat() {
System.out.println("吃");
}
}
class Dog extends Animal {
}
class Bird extends Animal {
public String wing;
public void fly() {
System.out.println("飞");
}
}
此时, Animal 这样被继承的类, 我们称为 父类 , 基类 或 超类, 对于像 Cat 和 Bird 这样的类, 我们称为 子类, 派生类和现实中的儿子继承父亲的财产类似, 子类也会继承父类的字段和方法, 以达到代码重用的效果。
语法规则
基本语法:
class 子类 extends 父类 {
}
使用 extends 指定父类。 Java 中一个子类只能继承一个父类 (而C++/Python等语言支持多继承)。 子类会继承父类的所有 public 的字段和方法。 对于父类的 private 的字段和方法, 子类中是无法访问的。 子类的实例中, 也包含着父类的实例. 可以使用 super 关键字得到父类实例的引用。 extends 英文原意指 “扩展”. 而我们所写的类的继承, 也可以理解成基于父类进行代码上的 “扩展”。例如我们写的 Bird 类, 就是在 Animal 的基础上扩展出了 fly 方法。
如果我们把 name 改成 private, 那么此时子类就不能访问了。
class Animal {
private String name;
public int age;
public void eat() {
System.out.println("吃");
}
}
class Bird extends Animal {
public String wing;
public void fly() {
System.out.println(name +"飞");
}
}
在这里插入图片描述
protected关键字
刚才我们发现, 如果把字段设为 private, 子类不能访问. 但是设成 public, 又违背了我们 “封装” 的初衷。两全其美的办法就是 protected 关键字。
对于类的调用者来说, protected 修饰的字段和方法是不能访问的。 对于类的 子类 和 同一个包的其他类 来说, protected 修饰的字段和方法是可以访问的。
也就是说如果字段被protected所修饰,那么想要调用该字段,要被是子类进行调用,要么是同一个包下调用,否则不允许访问。
小结: Java 中对于字段和方法共有四种访问权限
private: 类内部能访问, 类外部不能访问 默认(也叫包访问权限): 类内部能访问, 同一个包中的类可以访问, 其他类不能访问。 protected: 类内部能访问, 子类和同一个包中的类可以访问, 其他类不能访问。 public : 类内部和类的调用者都能访问。
我们通过表格以便进一步了解:
NO 范围 private default protected public
1 同一包中的同一个类 √ √ √ √
2 同一包中的不同类 √ √ √
3 不同包中的子类 √ √
4 不同包中的非子类 √
那么我们什么时候下用哪一种呢?
我们希望类要尽量做到 “封装”, 即隐藏内部实现细节, 只暴露出 必要 的信息给类的调用者。 因此我们在使用的时候应该尽可能的使用 比较严格 的访问权限。 例如如果一个方法能用 private, 就尽量不要用public。 另外, 还有一种 简单粗暴 的做法: 将所有的字段设为 private, 将所有的方法设为 public.。不过这种方式属于是对访问权限的滥用, 还是更希望能写代码的时候认真思考, 该类提供的字段方法到底给 “谁” 使用(是类内部自己用, 还是类的调用者使用, 还是子类使用)。
更为复杂的继承关系
在我们刚刚的例子中,我们只是涉及到Dog,Bird,Animal三种类,如果更为复杂的类关系,我们如何处理呢?
在这里插入图片描述
这时候采用继承方法来表示就会涉及到更为复杂的体系。这种继承方式称为多层继承,即子类还可以进一步的再派生出新的子类。但是我们并不希望类之间的继承关系层次太复杂,一般我们不希望出现超过三层的继承关系,如果继承层次太多,就需要考虑对代码进行重构了。如果想从语法上进行限制继承,就可以使用final关键字。
final关键字
曾经我们学习过 final 关键字, 修饰一个变量或者字段的时候, 表示 常量 (不能修改)。比如:
final int a = 10;
a = 20; // 编译出错
这时就会报出错误,此时的a是不可以被修改的。
final 关键字也能修饰类, 此时表示被修饰的类就不能被继承。
final public class Animal {
…
}
public class Bird extends Animal {
…
}
这时编译器也会报出错误,因为Animal无法被继承。因此final作为修饰类关键字,就是限制被修饰类的继承。
组合
和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果。
例如表示一个学校:学校有学生,有老师。
public class Student {
…
}
public class Teacher {
…
}
public class School {
public Student[] students;
public Teacher[] teachers;
}
组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段。这是我们设计类的一种常用方式之一。
多态
在了解多态前,我们需要了解几个概念。
向上转型
举个例子:
bird bird = new bird(“嘿嘿”, 19, “翅膀”);
Animal animal = bird;
或者
Animal animal =new bird(“嘿嘿”,19,“翅膀”);
此时 bird2 是一个父类 (Animal) 的引用, 指向一个子类 (Bird) 的实例. 这种写法称为向上转型。因为鸟是动物,因此就可以用父类引用(animal)引用子类实例(bird)。
什么时候会用到向上转型呢?
直接赋值 方法传参 方法返回
直接赋值的方式我们已经演示了. 另外两种方式和直接赋值没有本质区别。
方法传参:
public class TestDemo {
public static void main(String[] args) {
Bird bird = new Bird(“嘿嘿”, 19, “翅膀”);
sleep(bird);
}
public static void sleep(Animal animal){
System.out.println(“睡觉”);
}
}
此时形参 animal 的类型是 Animal (基类), 实际上对应到 Bird (父类) 的实例。
方法返回:
public static void main(String[] args) {
Animal animal = FindBird();
}
public static Animal FindBird() {
Bird bird = new Bird("嘿嘿", 19, "翅膀");
return bird;
}
此时方法 findBird 返回的是一个 Animal 类型的引用, 但是实际上对应到 Bird 的实例。
动态绑定
当子类和父类中出现同名方法的时候, 再去调用会出现什么情况呢?
对前面的代码稍加修改, 给 Bird 类也加上同名的 eat 方法, 并且在两个 eat 中分别加上不同的日志。
class Animal {
public String name;
public int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println("吃");
}
}
class Bird extends Animal {
public String wing;
public Bird(String name, int age, String wing) {
super(name, age);
this.wing = wing;
}
@Override
public void eat() {
System.out.println("小鸟吃");
}
public void fly() {
System.out.println("飞");
}
}
public class TestDemo {
public static void main(String[] args) {
Animal animal1 = new Animal(“哈哈”,20);
Animal animal2 = new Bird(“嘿嘿”, 19, “翅膀”);
animal1.eat();
animal2.eat();
}
}
此时,我们发现:
animal1 和 animal2 虽然都是 Animal 类型的引用, 但是 animal1 指向 Animal 类型的实例, animal2 指向Bird 类型的实例。 针对 animal1 和 animal2 分别调用 eat 方法, 发现 animal1.eat() 实际调用了父类的方法, 而animal2.eat() 实际调用了子类的方法。
因此, 在 Java 中, 调用某个类的方法, 究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) ,,要看究竟这个引用指向的是父类对象还是子类对象。这个过程是程序运行时决定的(而不是编译期),,因此称为动态绑定。
方法重写
针对刚才的 eat 方法来说,子类实现父类的同名方法,并且参数的类型和个数完全相同, 这种情况称为覆写/重写/覆盖(Override)。
关于重写的注意事项:
重写和重载完全不一样. 不要混淆。 普通方法可以重写, static 修饰的静态方法不能重写,final修饰的方法不能重写。 重写中子类的方法的访问权限不能低于父类的方法访问权限(private除外,private只能在类里面使用,Bird类访问不到)。 重写的方法返回值类型不一定和父类的方法相同。(但是建议最好写成相同, 特殊情况除外)。
另外, 针对重写的方法, 可以使用 @Override 注解来显式指定。
// Bird.java
public class Bird extends Animal {
@Override
private void eat(String food) {
…
}
}
有了这个注解能帮我们进行一些合法性校验。例如不小心将方法名字拼写错了 (比如写成 aet),那么此时编译器就会发现父类中没有 aet 方法,就会编译报错,提示无法构成重写。
注:重载和重写的区别。
NO 区别 重载(overload) 重写(override)
1 概念 方法名称相同,参数的类型及个数不同 方法名称、返回值类型、参数的类型及个数完全相同
2 范围 一个类 继承关系
3 限制 没有权限要求 被重写的方法不能拥有比父类更严格的访问控制权限
向下转型
关于向下转型是不安全的,就类似于把long类型给int类型,就像说动物是只鸟一样,真正使用的时候也不多,这里就不过多介绍了。
理解多态
有了面的向上转型,动态绑定,方法重写之后,我们就可以使用多态(polypeptide) 的形式来设计程序了。
举个例子:
class Shape {
public void draw() {
System.out.println(“Shape:draw()”);
}
}
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 Rect();
shape1.draw();
Shape shape2 = new Flower();
shape2.draw();
}
}
在这里插入图片描述
这样能完成打印的工作,我们在换一种写法:
public class Test {
public static void drawMap(Shape shape) {
shape.draw();
}
public static void main(String[] args) {
Rect rect = new Rect();
drawMap(rect);
Flower flower = new Flower();
drawMap(flower);
}
}
在这里插入图片描述
当类的调用者在编写 drawMap 这个方法的时候,参数类型为 Shape (父类),此时在该方法内部并不知道,也不关注当前的 shape 引用指向的是哪个类型(哪个子类)的实例。此时 shape 这个引用调用 draw 方法可能会有多种不同的表现(和 shape 对应的实例相关),这种行为就称为多态。
为什么要使用多态,使用多态的好处又是什么?
类调用者对类的使用成本进一步降低。 多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可 能够降低代码的 “圈复杂度”, 避免使用大量的 if - else。(圈复杂度是一种描述一段代码复杂程度的方式。一段代码如果平铺直叙,那么就比较简单容易理解。而如果有很多的条件分支或者循环语句,就认为理解起来更复杂)。 可扩展能力更强。 例如:如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低。
super关键字
前面的代码中由于使用了重写机制,调用到的是子类的方法。如果需要在子类内部调用父类方法怎么办? 可以使用super 关键字。
super 表示获取到父类实例的引用。 涉及到两种常见用法。
(1)使用了 super 来调用父类的构造器(这个代码前面已经写过了)。
public Bird(String name) {
super(name);
}
1 2 3
(2)使用 super 来调用父类的普通方法
public class Bird extends Animal {
public Bird(String name) {
super(name);
}
@Override
public void eat(String food) {
// 修改代码, 让子调用父类的接口.
super.eat(food);
System.out.println(“我是一只小鸟”);
System.out.println(this.name + “正在吃” + food);
}
}
在这个代码中,如果在子类的 eat 方法中直接调用 eat (不加super),那么此时就认为是调用子类自己的 eat (也就是递归了)。而加上 super 关键字,才是调用父类的方法。
注意 super 和 this 功能有些相似, 但是还是要注意其中的区别。
NO 区别 this super
1 概念 访问本类中的属性和方法 由子类访问父类中的属性和方法
2 查找范围 先查找本类,如果本类没有就调用父类 不查找本类而直接调用父类
3 特殊 表示当前对象 无
抽象类
在刚才理解多态中打印图形的例子中,我们发现,父类Shape中的draw方法好像并没有什么实际的工作,主要的绘制图形都是由Shape的各种子类的draw方法来完成的。像这种没有实际工作的方法,我们可以把它设计成一个抽象方法,而包含抽象方法的类我们称为抽象类。
abstract class Shape {
abstract public void draw();
}
在draw方法前面加上abstract关键字,表示这是一个抽象方法。同时抽象方法没有具体的方法实现过程。 对于包含抽象方法的类,必须加上abstract关键字表示这时一个抽象类。
抽象类的作用
抽象类的最大作用就是为了被继承。抽象类本身是不能被实例化的,要想使用只能通过创建该抽象类的子类,然后让子类重写抽象类中的抽象方法。有的同学会问,普通类也可以被继承,普通方法也可以被重写,为什么还要使用抽象类和抽象方法呢?的确如此,在Shape类中就是如此,但是如果将Shape改为抽象类,draw()方法改为抽象方法,在子类中就必须重写draw()方法,相当于多了一层编译器的校验。
总结
我们将关于抽象类的重点总结一下,这里就不举例来介绍了。
包含抽象方法的类,叫做抽象类。 一个没有具体实现的方法,被abstract修饰的方法叫做抽象方法。 抽象类是不可以被实例化的。(不能通过new实例化对象)。 由于不能被实例化,抽象类只能被继承。 抽象类中也可以包含和普通类一样的成员变量和普通方法。 一个普通类继承了一个抽象类,那么这个普通类当中,需要重写这个抽象类的所有的抽象方法。 抽象类的最大作用就是被继承,因此不能用final修饰。 一个抽象类A,如果继承了一个抽象类B,那么这个抽象类A,可以不实现抽象父类B的抽象方法。 结合第8点,当抽象类A再次被一个普通类继承后,普通类需要重写A和B中所有的抽象方法。
接口
接口是抽象类的更进一步。抽象类中还可以包含非抽象方法,和字段。而接口中包含的方法都是抽象方法,字段只能包含静态常量。
语法规则
在刚才的打印图形的示例中, 我们的父类 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 继承接口。此时表达的含义不再是 “扩展”,而是 “实现”。
在调用的时候同样可以创建一个接口的引用, 对应到一个子类的实例。
接口不能单独被实例化。
接口中只能包含抽象方法。对于字段来说,接口中只能包含静态常量(final static)。
interface IShape {
void draw();
public static final int num = 10;
}
其中的 public, static, final 的关键字都可以省略. 省略后的 num 仍然表示 public 的静态常量。
实现多个接口
有的时候我们需要让一个类同时继承自多个父类。这件事情在有些编程语言通过多继承的方式来实现的。然而我们直到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 + "正在蹬腿游泳");
}
}
class Duck extends Animal implements IRunning, ISwimming, IFlying {
public Duck(String name) {
super(name);
}
@Override
public void fly() {
System.out.println(this.name + "正在用翅膀飞");
}
@Override
public void run() {
System.out.println(this.name + "正在用两条腿跑");
}
@Override
public void swim() {
System.out.println(this.name + "正在漂在水上");
}
}
使用interface来修饰。 接口当中的普通方法,不能有具体的实现。非要实现,只能通过关键字default来修饰这个方法。 接口当中,可以用静态方法(static修饰)。 里面所有的方法都是public的。 所有方法都是抽象方法(default和static修饰的除外),默认是public abstract的。 接口是不可以被通过关键字new来实例化的。 类和接口之间的关系是通过implements实现的。 当一个类实现了一个接口,就必须要重写接口当中的抽象方法。 接口中是可以有成员变量的,默认是public static final修饰的,即是常量。 当一个类实现一个接口之后,重写这个方法的时候,这个方法前面必须加上public。 一个类可以通过关键字extends继承一个抽象类或者普通类,但是只能继承一个类。同时,也可以通过implements实现多个接口,接口之间用逗号隔开。 接口与接口之间是implements操作他们的关系。例如:一个接口B通过extends来拓展另一个接口C的功能。此时当一个类D通过implements实现这个接口B的接口的时候,此时重写的方法不仅仅是B的抽象方法,还有从C接口拓展过来的方法。



