Java面向对象编程详解
目录
一、包
包(package) 是组织类的一种方式,使用包的主要目的是保证类的唯一性。
导入包中的类
Java
中已经提供了很多现成的类供我们使用。
public class Test {
public static void main(String[] args) {
java.util.Date date = new java.util.Date();
// 得到一个毫秒级别的时间戳
System.out.println(date.getTime());
}
}
上面的代码也可以写成这样:
import java.util.Date;
//import java.util.*;
public class Test {
public static void main(String[] args) {
Date date = new Date();
// 得到一个毫秒级别的时间戳
System.out.println(date.getTime());
}
}
如果需要使用 java.util
中的其他类, 可以使用 import java.util.*
:其中*
表示通配符,意味着导入这个包底下所有的类,但是Java在处理的时候,需要那个类,才会拿哪个类。而在C语言里面,通过include
关键字导入之后,就会把这个头文件里面的内容全部都拿过来。
1.1静态导入
使用 import static
可以导入包中的静态的方法和字段。
例一:
import static java.lang.System.*;
public class Test {
public static void main(String[] args) {
out.println("hello");
}
}
例二:
import static java.lang.Math.*;
import static java.lang.System.*;
public class Test {
public static void main(String[] args) {
out.println(max(10,20));
}
}
通过静态导入的代码可读性不高,所以静态导入用的很少。
1.2将类放到包中
基本规则:
- 在文件的最上方加上一个
package
语句指定该代码在哪个包中. - 包名需要尽量指定成唯一的名字, 通常会用公司的域名的颠倒形式(例如
-
包名要和代码路径相匹配. 例如创建
com.company.demo1
的包, 那么会存在一个对应的路径com/company/demo1
来存储代码. - 如果一个类没有
package
语句, 则该类被放到一个默认包中.
1.3包的访问权限控制
我们已经了解了类中的 public
和 private
. private
中的成员只能被类的内部使用.
如果某个成员不包含 public
和 private
关键字, 此时这个成员可以在包内部的其他类使用, 但是不能在包外部的类使用。
包的访问权限指的是只能在当前包当中进行使用,当你的成员变量不加任何访问修饰限定词的时候,它默认就是一个包访问权限。
同一个包下的成员变量可以访问,其他包下的成员变量无法从外部程序包中对其进行访问。
常见的系统包:
-
java.lang
:系统常用基础类(String、Object
),此包从JDK1.1
后自动导入。 -
java.lang.reflect
:java
反射编程包; -
java.net
:进行网络编程开发包。 -
java.sql
:进行数据库开发的支持包。 -
java.util
:是java提供的工具程序包。(集合类等) 非常重要。 -
java.io
:I/O
编程开发包。
二、继承
封装:不必要公开的数据成员和方法使用private
关键字进行修饰。意义:安全性。
继承:对共性的抽取。使用extends
关键字进行处理的。意义:可以对代码进行重复使用。
我们并不希望类之间的继承层次太复杂. 一般我们不希望出现超过三层的继承关系. 如果继承层次太多, 就需要考虑对代码进行重构了。
基本语法:
class 子类 extends 父类 {
}
其中Cat
这样的类, 我们称为子类, 派生类,Animal
这样被继承的类, 我们称为 父类 , 基类 或 超类。
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.name = "小黑";
System.out.println(cat.name);
cat.eat("猫粮");
Bird bird = new Bird();
bird.name = "圆圆";
System.out.println(bird.name);
bird.fly();
}
}
[注意]:
- 使用
extends
继承父类. -
Java
中一个子类只能继承一个父类 (而C++/Python
等语言支持多继承).即Java当中是单继承,不能同时继承两个类以上的类,包括两个。 - 子类会继承父类的所有
public
的字段和方法. - 对于父类的
private
的字段和方法, 子类中是无法访问的. - 子类的实例中, 也包含着父类的实例. 可以使用
super
关键字得到父类实例的引用。
2.1 super关键字
super
:不能出现在静态方法中,其代表的是父类实例的引用。
super的两种常见用法:
- 使用了 super 来调用父类的构造器。
public Cat(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
关键字。
子类构造的同时,要先帮助父类来进行构造。需要在子类的构造方法当中,使用super
关键字来显示的调用父类的构造方法。
super有3中表现形式:
- super();//调用父类的构造方法
- super.func();
- super.data;
super与this的区别
super
的概念:表示子类访问父类的属性、方法。不查找本类,而直接调用父类定义。
this
的概念:访问本类中的属性、方法,其先查找本类,如果本类没有就调用父类。
2.2 protected 关键字
protected 关键字:对于类的调用者来说, protected 修饰的字段和方法是不能访问的,对于类的子类和同一个包的其他类 来说, protected 修饰的字段和方法是可以访问的。
Java 中对于字段和方法共有四种访问权限
-
private
: 类内部能访问, 类外部不能访问。 - 默认(也叫包访问权限): 类内部能访问, 同一个包中的类可以访问, 其他类不能访问.
-
protected
: 类内部能访问, 子类和同一个包中的类可以访问, 其他类不能访问. -
public
: 类内部和类的调用者都能访问。
也就是说:
字段如果是public
,在哪里使用都可以。
private
只有在同一个包的同一个类中才能使用。
default
在同一个包的同一个类及同一包中的不同类中可以使用。
protected
在同一个包的同一个类中,同一包中的不同类中,及不同包中得子类中可以使用。
2.3 final 关键字
如果一个类不想被继承,我们可以设置为final
修饰。
final
关键字修饰类, 此时表示被修饰的类就不能被继承。
1.final int a = 20;//常量,不可以被修改
2.final Class A{ //代表整个类不可以被继承
}
3.final修饰方法
三、组合
和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果。
组合并没有涉及到特殊的语法(诸如extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段.这是我们设计类的一种常用方式之一。
如一个学校:
public class Student {
...
}
public class Teacher {
...
}
public class School {
public Student[] students;
public Teacher[] teachers;
}
组合表示 has - a
语义。
如:一个学校中 “包含” 若干学生和教师.
继承表示 is - a
语义。
在上面的 “动物和猫” 的例子中, 我们可以理解成一只猫也 “是” 一种动物。
四、多态
4.1向上转型
向上转型:表示往父类的方向转。
向上转型发生的时机:
- 直接赋值
在上面的例子中有:
public class Test {
public static void main(String[] args) {
//Cat cat = new Cat();
//Animal animal = cat;
Animal animal = new Cat();//父类引用 引用子类对象
}
}
- 作为函数的参数,方法传参
public class Test {
public static void function(Animal ani){
}
public static void main(String[] args) {
//Cat cat = new Cat();
//Animal animal = cat;
Animal animal = new Cat();//父类引用 引用子类对象
Cat cat = new Cat();
function(cat);
}
}
此时形参 ani
的类型是 Animal
(基类), 实际上对应到 Cat
(父类) 的实例。
- 作为方法返回
public class Test {
public static Animal func2(Animal ani){
Cat cat = new Cat();
return cat;
}
public static void main(String[] args) {
Animal animal = func2();
}
}
此时方法 func2
返回的是一个 Animal
类型的引用, 但是实际上对应到 Cat
的实例。
4.2 动态绑定
编译的时候不能够确定到底调用谁的方法。运行的时候,才知道了调用哪个方法。这就叫做运行时绑定也就是动态绑定。动态绑定也是多态的基础。
1、父类引用引用子类的对象。
2、运行时绑定:通过这个父类引用调用父类和子类同名的覆盖方法。
同名的覆盖方法也叫重写/覆写/覆盖:
1、方法名相同
2、参数列表相同(参数的个数+参数的类型)
3、返回值相同 (特殊:返回值也可以是协变类型)
4、父子类的的情况下
关于重写的注意事项:
- 重写和重载完全不一样.
- 普通方法可以重写,
static
修饰的静态方法不能重写. - 重写中子类方法的访问权限不能低于(大于等于)父类的访问限定.
-
private
方法不能够重写. - 被
final
修饰的方法不能够重写. - 重写的方法返回值类型不一定和父类的方法相同(但是建议最好写成相同, 特殊情况除外).
编译时绑定:通过函数的重载实现的。编译的时候,会根据你给的参数的个数和类型,在编译期,确定你最终调用的方法。
重载和重写的区别
重载:方法名相同,参数的类型和个数不同,重载的范围是一个类。
重写:方法名,返回值类型、参数类型及个数完全相同,范围是继承关系,被覆写的方法不能拥有比父类更严格的访问控制权限。
通过父类引用只能访问父类自己的成员。
4.3 向下转型
向上转型是子类对象转成父类对象, 向下转型就是父类对象转成子类对象. 相比于向上转型来说, 向下转型没那么常见,但是也有一定的用途。
比如:
public static void main(String[] args) {
Animal animal = new Bird();
Bird bird = (Bird)animal;
bird.fly();
}
向下转型不安全,不是所有的类型都具有向下转型中得方法,此时,我们可以加上instanceof
判断一下,可以判断出一个引用是否是某个类的实例。如:
public class Test {
public static void main(String[] args) {
Animal animal = new Cat();
if(animal instanceof Bird){
Bird bird = (Bird)animal;
bird.fly();
}
}
}
4.4 理解多态
class Shape{
public void draw(){
System.out.println("Shape::draw()");
}
}
class Rect extends Shape{
public void draw(){
System.out.println("♦");
}
}
class Flower extends Shape{
public void draw(){
System.out.println("❀");
}
}
class Triangle extends Shape{
@Override
public void draw() {
System.out.println("△");
}
}
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);
Triangle triangle = new Triangle();
drawMap(triangle);
}
}
当类的调用者在编写 drawMap
这个方法的时候, 参数类型为 Shape
(父类), 此时在该方法内部并不知道, 也不关注当前的 shape
引用指向的是哪个类型(哪个子类)的实例. 此时 shape
这个引用调用 draw
方法可能会有多种不同的表现
(和 shape
对应的实例相关), 这种行为就称为多态。
使用多态的好处:
- 类调用者对类的使用成本进一步降低
- 封装是让类的调用者不需要知道类的实现细节.
- 多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可.因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低.
- 能够降低代码的 “圈复杂度”, 避免使用大量的
if - else
圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂.因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 “圈复杂度”. 如果一个方法的圈复杂度太高, 就需要考虑重构。
如:
public class Test {
public static void drawMap(Shape shape){
shape.draw();
}
public static void main(String[] args) {
Rect rect = new Rect();
Flower flower = new Flower();
Triangle triangle = new Triangle();
Shape[] shapes = {triangle, rect, triangle, rect, flower};
for (Shape shape:shapes) {
shape.draw();
}
}
}
如果使用 if - else
则实现如下:
public static void main(String[] args) {
Rect rect = new Rect();
Flower flower = new Flower();
Triangle triangle = new Triangle();
String[] shapes = {"triangle", "rect", "triangle", "rect", "flower"};
for (String s:shapes) {
if(s.equals("triangle")){
triangle.draw();
}else if(s.equals("rect")){
rect.draw();
}else if (s.equals("flower")){
flower.draw();
}
}
- 可扩展能力更强.
如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低.
class Cycle extends Shape {
@Override
public void draw() {
System.out.println("●");
}
}
五、抽象类
在刚才的打印图形例子中, 我们发现, 父类 Shape 中的 draw 方法好像并没有什么实际工作, 主要的绘制图形都是由Shape 的各种子类的 draw 方法来完成的. 像这种没有实际工作的方法, 我们可以把它设计成一个 抽象方法(abstract method)。
//抽象类
abstract class Shape{
//抽象方法
public abstract void draw();
}
注意:
- 包含抽象方法的类我们称为抽象类(abstract class)。
- 抽象方法是被abstract修饰的,没有具体实现的方法。
- 抽象类是不可以被实例化的。
- 由于抽象类不能被实例化,所以只能被继承。
- 抽象类当中也可以包含和普通类一样的成员和方法。抽象类中可以包含其他的非抽象方法, 也可以包含字段. 这个非抽象方法和普通方法的规则都是一样的, 可以被重写,也可以被子类直接调用。
- 一个普通类,继承了一个抽象类,那么这个普通类当中需要重写这个抽象类的所有的抽象方法。
- 抽象类最大的作用,就是为了被继承。
- 一个抽象类A,如果继承了一个抽象类B,那么这个抽象类A,可以不实现抽象父类B的抽象方法。
- 结合第8点,当A类再次被一个普通类继承后,那么A和B这两个抽象类当中的抽象方法,必须被重写。
- 抽象类不能被
final
修饰,抽象方法也不可以被final
修饰。 - 抽象方法不能是
private
修饰的。
使用抽象类相当于多了一重编译器的校验,使用抽象类的场景就如上面的代码, 实际工作不应该由父类完成, 而应由子类完成. 那么此时如果不小心误用成父类了,
使用普通类编译器是不会报错的. 但是父类是抽象类就会在实例化的时候提示错误, 让我们尽早发现问题.
六、接口
- 接口是使用
interface
来修饰的。interface IA{}
- 接口当中的普通方法,不能有具体的实现。非要实现,只能通过关键字
default
来修饰这个方法。 - 接口当中,可以有
static
的方法。 - 接口里面的所有的方法都是
public
的,因此可以省略public
。 - 抽象方法,默认是
public abstract
的。
//接口是使用interface来修饰的
interface IShape{
//抽象方法,默认是public abstract的。
public abstract void func1();
//接口当中的普通方法,不能有具体的实现。非要实现,只能通过关键字`default`来修饰这个方法
default public void draw(){
System.out.println("aaa");
}
//接口当中,可以有static的方法。
public static void func(){
System.out.println("bbb");
}
}
- 接口是不可以被通过关键字
new
来实例化的。 - 类和接口之间的关系是通过
implements
实现了。 - 当一个类实现了一个接口,就必须要重写接口当中的抽象方法。
- 接口中的方法一定是抽象方法, 因此可以省略
abstract
。 - 接口当中的成员变量,默认是
public static final
修饰的。 - 当一个类实现一个接口之后,重写这个方法的时候,这个方法前面必须加上
public
。
interface IA{
//public static final int a = 10;//public static final可以省略
int A1 = 10;
void funcA();//默认为抽象方法
}
class AClass implements IA{
public void funcA(){
}
}
- 一个类可以通过关键字
extends
继承一个抽象类或者普通类,但是只能继承一个类。同时,也可以通过implements
实现多个接口,接口之间使用逗号隔开就好。 - 接口中只能包含抽象方法. 对于字段来说, 接口中只能包含静态常量(final static).
如:
interface IA{
public static final int a = 10;
int A = 10;
void funcA();//默认为抽象方法
}
interface IB{
void funcB();
}
class BClass {
}
class AClass extends BClass implements IA,IB{
public void funcA(){
System.out.println("A::funcA()");
System.out.println(A);
}
@Override
public void funcB() {
System.out.println("A::funcB()");
}
}
- 接口和接口之间可以使用
extends
来操作他们的关系,此时,这里面意为:拓展。一个接口IB1
通过extends
来拓展另一个接口IA1
的功能。此时当一个类C
通过implements
实现这个接口IB1
的时候,此时重写的方法不仅仅是IB1
的抽象方法,还有他从IA1
接口拓展来的功能/方法。
如下所示:
interface IA1{
void funcA();//默认为抽象方法
}
interface IB1 extends IA1{
void funcB();
}
class C implements IB1{
@Override
public void funcA() {
System.out.println("aaa");
}
@Override
public void funcB() {
System.out.println("bbb");
}
}
使用接口也可以实现动态绑定,比如下面的例子:
interface IShape{
// public abstract void draw();
void draw();
}
class Rect implements IShape{
public Rect() {
}
@Override
public void draw() {
System.out.println("fangPain");
}
}
class Flower implements IShape {
public void draw(){
System.out.println("hua");
}
}
class Triangle implements IShape{
@Override
public void draw() {
System.out.println("△");
}
}
class Cycle implements IShape {
@Override
public void draw() {
System.out.println("●");
}
}
public class Test {
public static void drawMap( IShape iShape){
iShape.draw();
}
public static void main(String[] args) {
Flower flower = new Flower();
Cycle cycle = new Cycle();
Triangle triangle = new Triangle();
drawMap(flower);
drawMap(cycle);
drawMap(triangle);
}
}
实现多个接口
有的时候我们需要让一个类同时继承自多个父类. 这件事情在有些编程语言通过 多继承的方式来实现的.然而 Java 中只支持单继承, 一个类只能 extends 一个父类. 但是可以同时实现多个接口, 也能达到多继承类似的效果.
例:
class Animal{
protected String name;
public Animal(String name) {
this.name = name;
}
}
//不是所有的动物都会飞,所以不能写到Animal类当中
//如果写到别的类当中也不行,因为一个类不能继承多个类,所以用到接口
interface IFlying{
void fly();
}
interface IRunning{
void run();
}
interface ISwimming{
void swimming();
}
class Bird extends Animal implements IFlying{
public Bird(String name) {
super(name);
}
@Override
public void fly() {
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 swimming() {
System.out.println(this.name + "正在游泳");
}
}
class Duck extends Animal implements ISwimming,IRunning,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 swimming() {
System.out.println(this.name + "正在游泳");
}
}
public class test04 {
public static void runFunc(IRunning iRunning){
iRunning.run();
}
public static void swimmingFunc(ISwimming iSwimming){
iSwimming.swimming();
}
public static void flyingFunc(IFlying iFlying){
iFlying.fly();
}
public static void main(String[] args) {
runFunc(new Duck("鸭子"));
runFunc(new Frog("青蛙"));
swimmingFunc(new Duck("鸭子"));
flyingFunc(new Bird("小鸟"));
flyingFunc(new Duck("鸭子"));
}
}
有了接口之后, 类的使用者就不必关注具体类型, 而只关注某个类是否具备某种能力。
三个常用的接口
如果自定义的数据类型进行大小的比较一定要实现可以比较的接口。如
Comparable
这个接口有一个很大的缺点:对类的侵入性非常强。一旦写好了,不敢轻易改动。
例:
class Student implements Comparable<Student> {
public String name;
public int age;
public double score;
public Student(String name, int age, double score) {
this.name = name;
this.age = age;
this.score = score;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + ''' +
", age=" + age +
", score=" + score +
'}';
}
@Override
public int compareTo(Student o) {//谁调用compareTo,谁就是this
// if (this.age > o.age){
// return 1;
// }else if (this.age < o.age){
// return -1;
// }else {
// return 0;
// }
//return this.age - o.age;//从小到大排序
//return o.age - this.age;//从大到小排序
//return (int)(this.score - o.score);
return this.name.compareTo(o.name);
}
}
Comparator
灵活,对类的侵入性非常弱。
例:
class Student {
public String name;
public int age;
public double score;
public Student(String name, int age, double score) {
this.name = name;
this.age = age;
this.score = score;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + ''' +
", age=" + age +
", score=" + score +
'}';
}
}
class AgeComparator implements Comparator<Student>{
@Override
public int compare(Student o1, Student o2) {
return o1.age - o2.age;
}
}
class ScoreComparator implements Comparator<Student>{
@Override
public int compare(Student o1, Student o2) {
return (int)(o1.score - o2.score);
}
}
class NameComparator implements Comparator<Student>{
@Override
public int compare(Student o1, Student o2) {
return o1.name.compareTo(o2.name);
}
}
public class test01 {
public static void main(String[] args) {
Student[] student = new Student[3];
student[0] = new Student("zHang",12,100);
student[1] = new Student("aWang",18,98);
student[2] = new Student("li",56,88);
System.out.println(Arrays.toString(student));//排序前打印
AgeComparator ageComparator = new AgeComparator();
ScoreComparator scoreComparator = new ScoreComparator();
NameComparator nameComparator = new NameComparator();
Arrays.sort(student,ageComparator);//默认是从小到大的排序
System.out.println(Arrays.toString(student));//排序后打印
}
}
Comparable、Comparator
用哪个接口取决于你的业务,一般不叫推荐比较器
Cloneable
未完待续…
以上。