我们都知道Java是一门面向对象编程的语言,而我们要面向对象来进行编程,首先需要具有面向对象编程的思想。我们以包,继承,组合,多态,抽象类,接口来作为突破点来认识面向对象进行编程,而同时OOP语言的三大特征为:继承,封装,多态。面对对象的程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏的实现部分。而程序中的很多对象来自于标准库除此之外就是自定义的一些对象,而我们究竟是自己来构造对象还是从库中导入,取决于开发项目的具体要求,但是从根本上来说,只要对象能够满足要求,就不必要关心其功能的具体实现过程。
包
包 (package) 是组织类的一种方式,借助于包可以更方便的组织自己的代码,并将自己的代码与别人提供的代码库分开来进行管理,标准的Java类库分布于多个包中(java.lang java.util java.net等)我们也可以使用嵌套层次来组织包,所有标准的java包都处于java和javax包层次中。 使用包的主要目的是保证类名的唯一性。
- 导入包中的类
一个类可以使用所属包中的所有类,以及其他包中的公有类(public class)。我们可以用两种方式来访问另一个包中的公有类。第一种是在每个类名之前加完整的包名,另一种则是使用import语句。import语句是一种引用包含在包中的类的简明描述,利用该语句在使用类时,不必写出包的全名。
//第一种情况
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.*;
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 static 可以导入包中的静态的方法和字段。使用这种方式可以更方便的写一些代码,例如:
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);
}
}
- 将类放到包中
基本规则:
1.在文件的最上方加上一个 package 语句指定该代码在哪个包中。
2.包名需要尽量指定成唯一的名字, 通常会用公司的域名的颠倒形式(例如 com.bit.demo1 )。
3.包名要和代码路径相匹配. 例如创建 com.bit.demo1 的包, 那么会存在一个对应的路径 com/bit/demo1 来存储代码.
4.如果一个类没有 package 语句, 则该类被放到一个默认包中.
操作步骤:
- 在 IDEA 中先新建一个包: 右键 src -> 新建 -> 包
- 在弹出的对话框中输入包名, 例如 com.bit.demo1
- 在包中创建类, 右键包名 -> 新建 -> 类, 然后输入类名即可
- 此时可以看到我们的磁盘上的目录结构已经被 IDEA 自动创建出来了
目录结构:(D:\Java\Test\src\com\bit\demo1)
- 包的访问权限控制
我们已经了解了访问修饰符 public 和 private。 标记为 public 的部分可以被任意的类使 用;标记为private的部分只能被定义它们的类使用。如果没有指定public或private, 这个部 分(类、 方法或变量)可以被同一个包中的所有方法访问。 - 常见的系统包
java.lang:系统常用基础类(String、Object),此包从JDK1.1后自动导入。
java.lang.reflect:java 反射编程包;
java.net:进行网络编程开发包。
java.sql:进行数据库开发的支持包。
java.util:是java提供的工具程序包。(集合类等) 非常重要
java.io:I/O编程开发包。
继承
利用继承,可以基于已存在的类构造一个新类(代码重用)。继承已存在的类就是复用(继承)这些类的方法和域。 在此基础上, 还可以添加一些新的方法和域。
被继承的类, 我们称为 父类 , 基类 或 超类,对于继承的类, 我们称为子类, 派生类,例如Animal里包括Cat,Dog…Animal就是父类,Cat,Dog…即为子类。
而父类与子类之间满足什么条件可以发生继承呢?(以Animal,Cat, Dog为例)
1.这三个类都具备一个相同的方法, 而且行为是完全一样的.
2.这三个类都具备一个相同的属性, 而且意义是完全一样的.
3.从逻辑上讲, Cat 和 Dog都是一种 Animal (is - a 语义).
- 语法规则
class 子类 extends 父类 {
}
1.使用 extends 指定父类.
2.Java 中一个子类只能继承一个父类 (而C++/Python等语言支持多继承).
3.子类会继承父类的所有 public 的字段和方法.
4.对于父类的 private 的字段和方法, 子类中是无法访问的.
5.子类的实例中, 也包含着父类的实例. 可以使用 super 关键字得到父类实例的引用
-
protected 关键字
如果把字段设为 private, 子类不能访问. 但是设成 public, 又违背了我们 “封装” 的初衷.两全其美的办法就是 protected 关键字.
对于类的调用者来说, protected 修饰的字段和方法是不能访问的对于类的 子类 和 同一个包的其他类 来说, protected 修饰的字段和方法是可以访问的。
Java 中对于字段和方法共有四种访问权限
1.private: 类内部能访问, 类外部不能访问
2.默认(也叫包访问权限): 类内部能访问, 同一个包中的类可以访问, 其他类不能访问.
3.protected: 类内部能访问, 子类和同一个包中的类可以访问, 其他类不能访问.
4.public : 类内部和类的调用者都能访问. -
多层继承, 即子类还可以进一步的再派生出新的子类.
我们写的类是现实事物的抽象. 而我们真正在公司中所遇到的项目往往业务比较复杂, 可能会涉及到一系列复杂的概念, 都需要我们使用代码来表示, 所以我们真实项目中所写的类也会有很多. 类之间的关系也会更加复杂.但是即使如此, 我们并不希望类之间的继承层次太复杂. 一般我们不希望出现超过三层的继承关系. 如果继承层次太多, 就需要考虑对代码进行重构了.如果想从语法上进行限制继承, 就可以 final 关键字. -
final关键字
修饰一个变量或者字段的时候, 表示 常量 (不能修改)final 关键字也能修饰类, 此时表示被修饰的类就不能被继承.
组合
和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果.例如:
public class Student {
...
}
public class Teacher {
...
}
public class School {
public Student[] students;
public Teacher[] teachers;
}
组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段.这是我们设计类的一种常用方式之一.
组合表示 has - a 语义
在刚才的例子中, 我们可以理解成一个学校中 “包含” 若干学生和教师.
继承表示 is - a 语义
在上面的 “动物和猫” 的例子中, 我们可以理解成一只猫也 “是” 一种动物
多态
- 向上转型
在面向对象程序设计中, 针对一些复杂的场景(很多类, 很复杂的继承关系), 程序猿会画一种 UML 图的方式来表示类之间的关系. 此时父类通常画在子类的上方. 所以我们就称为 “向上转型” , )
例如这个代码: 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 的实例.
-
动态绑定
运行时绑定–》动态绑定(动多态)
发生的条件:
1.先要向上转型–》父类的引用 引用子类的对象
2.父类和子类都有同名的覆盖方法
因此, 在 Java 中, 调用某个类的方法, 究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) , 要看究竟这个引用指向的是父类对象还是子类对象. 这个过程是程序运行时决定的(而不是编译期), 因此称为 动态绑定. -
方法重写
子类实现父类的同名方法, 并且参数的类型和个数完全相同, 这种情况称为 覆写/重写/覆盖(Override).
关于重写的注意事项 -
重写和重载完全不一样. 不能混淆
-
普通方法可以重写, static 修饰的静态方法不能重写
-
重写中子类的方法的访问权限不能低于父类的方法访问权限.
-
针对重写的方法, 可以使用 @Override 注解来显式指定
重载和重写的区别:
-
理解多态
使用多态的好处是什么?
- 类调用者对类的使用成本进一步降低.
封装是让类的调用者不需要知道类的实现细节.
多态能让类的调用者连这个类的类型是什么都不必知道
只需要知道这个对象具有某个方法即可.
因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低.这也贴合了 <<代码大全>> 中关于 “管理代码复杂程度” 的初衷 - 能够降低代码的 “圈复杂度”, 避免使用大量的 if - else
- 可扩展能力更强
代码示例: 打印多种形状
lass 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 对应的实例相关), 这种行为就称为多态
-
向下转型
向上转型是子类对象转成父类对象, 向下转型就是父类对象转成子类对象. 相比于向上转型来说, 向下转型没那么常见,
但是也有一定的用途
向下转型的前提是有一次(之前)向上转型 。需要注意的是如果需要向下转换一种类型(子类),前提是向下转换的这种类型(子类)已经被父类引用。
instanceof :用来判断内存中实际对象(实例)A是不是B类型(左边是对象(实例)右边是类),用来做类的类型判断。
java.lang.ClassCastException: 类型转换异常 -
super 关键字
用于在在子类内部调用父类方法
super 表示获取到父类实例的引用. 涉及到两种常见用法
- 使用了 super 来调用父类的构造器(这个代码前面已经写过了)
public Bird(String name) {
super(name);
}
- 使用 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 功能有些相似, 但是还是要注意其中的区别
抽象类
例如没有实际工作的方法, 我们可以把它设计成一个 抽象方法(abstractmethod), 包含抽象方法的类我们称为 抽象类(abstract class).
语法规则:
//对于包含抽象方法的类, 必须加上 abstract 关键字表示这是一个抽象类.
abstract class Shape {
abstract public void draw();
}
注意事项:
- 抽象类不能直接实例化.
- 抽象方法不能是 private 的.
- 抽象类中可以包含其他的非抽象方法, 也可以包含字段. 这个非抽象方法和普通方法的规则都是一样的, 可以被重写,也可以被子类直接调用.
抽象类的作用:抽象类存在的最大意义就是为了被继承.
接口
接口是抽象类的更进一步. 抽象类中还可以包含非抽象方法, 和字段. 而接口中包含的方法都是抽象方法, 字段只能包含静态常量
语法规则
在刚才的打印图形的示例中, 我们的父类 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();
}
}
提示:
1.我们创建接口的时候, 接口的命名一般以大写字母 I 开头.
-
接口的命名一般使用 “形容词” 词性的单词.
-
阿里编码规范中约定, 接口中的方法和属性不要加任何修饰符号, 保持代码的简洁性
-
实现多个接口(提示, IDEA 中使用 ctrl + i 快速实现接口)
有的时候我们需要让一个类同时继承自多个父类. 这件事情在有些编程语言通过 多继承 的方式来实现的.然而 Java 中只支持单继承, 一个类只能 extends 一个父类. 但是可以同时实现多个接口, 也能达到多继承类似的效果.
Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口.继承表达的含义是 is - a 语义, 而接口表达的含义是 具有 xxx 特性 .有了接口之后, 类的使用者就不必关注具体类型, 而只关注某个类是否具备某种能力 -
接口使用实例
给对象数组排序给定一个学生类
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) {
Student s = (Student)o;
if (this.score > s.score) {
return -1;
} else if (this.score < s.score) {
return 1;
} else {
return 0;
}
}
}
在 sort 方法中会自动调用 compareTo 方法. compareTo 的参数是 Object , 其实传入的就是 Student 类型的对象.
然后比较当前对象和参数对象的大小关系(按分数来算).
如果当前对象应排在参数对象之前, 返回小于 0 的数字;
如果当前对象应排在参数对象之后, 返回大于 0 的数字;
如果当前对象和参数对象不分先后, 返回 0;再次执行程序, 结果就符合预期了.
// 执行结果
[[王五:97], [李四:96], [张三:95], [赵六:92]]
注意事项: 对于 sort 方法来说, 需要传入的数组的每个对象都是 “可比较” 的, 需要具备 compareTo 这样的能力. 通过重写 compareTo 方法的方式, 就可以定义比较规则.
- 接口间的继承
接口可以继承一个接口, 达到复用的效果. 使用 extends 关键字.
抽象类和接口核心区别: 抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写), 而接口中不能包含普通方法, 子类必须重写所有的抽象方法。