java面向对象编程思想(包,继承,多态,抽象类)

一.

1.包

使用包的目的是为了保证类的唯一性

Java 中已经提供了很多现成的类供我们使用.:

例如: 可以使用 java.util.Date

这种方式引入 java.util 这个包中的 Date 类. 

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.*;
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());   } } 
// 编译出错 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());
   }
}//correct 

注意事项:

import 和 C++ 的 #include 差别很大. C++ 必须 #include 来引入其他文件内容, 但是 Java 不需要. import 只是为了写代码的时候更方便. import 更类似于 C++ 的 namespace 和 using

 2.静态导入

使用 import static 可以导入包中的静态的方法和字段. 使用这种方式可以更方便的写一些代码, 例如 将类放到包中 基本规则 在文件的最上方加上一个 package 语句指定该代码在哪个包中. 包名需要尽量指定成唯一的名字, 通常会用公司的域名的颠倒形式(例如 com.b.demo1 ).

import static java.lang.System.*; 
public class Test {    
public static void main(String[] args) {        
out.println("hello");   
} 
} 
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);   } }

3.包的创建

接着输入包名  并在当前包中新建类  并给类命名 即可

4.包的访问权限控制

如果某个成员不包含 public 和 private 关键字, 此时这个成员可以在包内部的其他类使用, 但是不能在包外部的类使 用.请看如下代码:

Demo1.java
package com.b.demo;
public class Demo1 {
    int value = 0;
}
Demo2.java
package com.b.demo; 
public class Demo2 { 
 public static void Main(String[] args) { 
 Demo1 demo = new Demo1(); 
 System.out.println(demo.value); 
 } 
} // 执行结果, 能够访问到 value 变量 结果为10 
 
import com.b.demo.Demo1; 
public class Test { 
 public static void main(String[] args) { 
 Demo1 demo = new Demo1(); 
 System.out.println(demo.value); 
 } 
} // 编译出错
Error:(6, 32) java: value在com.b.demo.Demo1中不是公共的; 无法从外部程序包中对其进行访问

5.常见的系统包

 java.lang:系统常用基础类(String、Object),此包从JDK1.1后自动导入。

 java.lang.reflect:java 反射编程包;

java.net:进行网络编程开发包。

java.sql:进行数据库开发的支持包。

java.util:是java提供的工具程序包。(集合类等) 非常重要

java.io:I/O编程开发包。

二.继承(extends)

相似的事物具有很多相同的属性 这时如果抽象出来一个父类 并让子类进行继承 就会很大程度避免代码的冗余 父类我们称 父类 , 基类 或 超类, 对于子类, 我们称为 子类, 派生类 和现实中的儿子继承父亲的财产类似, 子类也会继承父类的字段和方法, 以达到代码重用的效果,格式如下

class 子类 extends 父类 { 
 
} 

使用 extends 指定父类:

Java 中一个子类只能继承一个父类 (而C++/Python等语言支持多继承).(个人认为比较重要)

子类会继承父类的所有 public 的字段和方法.(继承父类构造器也会执行)

对于父类的 private 的字段和方法, 子类中是无法访问的.(但是依旧可以认为其继承 空间依旧占用 不过不可调用而已)

子类的实例中, 也包含着父类的实例.

可以使用 super 关键字得到父类实例的引用,请看如下代码段:

class Animal { 
 public String name; //private 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 + "正在飞 ︿( ̄︶ ̄)︿"); 
} 
} // 编译出错 Error:(19, 32) java: name 在 Animal 中是 private 访问控制

 对于类的调用者来说, protected 修饰的字段和方法是不能访问的

Animal.java 
public class Animal { 
protected String name; 
public Animal(String name) { 
this.name = name; 
} 
public void eat(String food) { 
System.out.println(this.name + "正在吃" + food); } } 
Bird.java 
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 } } 

对于类的 子类 和 同一个包的其他类 来说, protected 修饰的字段和方法是可以访问的

小结: Java 中对于字段和方法共有四种访问权限

private: 类内部能访问, 类外部不能访问

默认(也叫包访问权限): 类内部能访问, 同一个包中的类可以访问, 其他类不能访问.

protected: 类内部能访问, 子类和同一个包中的类可以访问, 其他类不能访问.

public : 类内部和类的调用者都能访问 

什么时候下用哪一种呢?

我们希望类要尽量做到 "封装", 即隐藏内部实现细节, 只暴露出 必要 的信息给类的调用者. 因此我们在使用的时候应该尽可能的使用 比较严格 的访问权限. 例如如果一个方法能用 private, 就尽量不要用 public. 另外, 还有一种 简单粗暴 的做法: 将所有的字段设为 private, 将所有的方法设为 public. 不过这种方式属于是对 访问权限的滥用, 还是更希望同学们能写代码的时候认真思考, 该类提供的字段方法到底给 "谁" 使用(是类内部 自己用, 还是类的调用者使用, 还是子类使用).

引入多层继承及final: 我们写的类是现实事物的抽象. 而我们真正在公司中所遇到的项目往往业务比较复杂, 可能会涉及到一 系列复杂的概念, 都需要我们使用代码来表示, 所以我们真实项目中所写的类也会有很多. 类之间的关系也会更加 复杂. 但是即使如此, 我们并不希望类之间的继承层次太复杂. 一般我们不希望出现超过三层的继承关系. 如果继承层 次太多, 就需要考虑对代码进行重构了. 如果想从语法上进行限制继承, 就可以使用 final 关键字

2.final

final :  修饰一个变量或者字段的时候, 表示 常量 (不能修改).

final int a = 10; 
a = 20; // 编译出错

 final 关键字也能修饰类, 此时表示被修饰的类就不能被继承.

final public class Animal { ... } p
ublic class Bird extends Animal { ... } // 编译出错 Error:(3, 27) java: 无法从最终com.b.Animal进行继承 final 

关键字的功能是限制类被继承 "限制" 这件事情意味着 "不灵活".用 final 修饰的类被继承的时候, 就会编译报错, 此时就可以提示我们这样的继承是有悖这个类设计的初衷的.

我们平时是用的 String 字符串类, 就是用 final 修饰的, 不能被继承

3.组合(has-a||is-a)

和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果.

例如表示一个学校: 

public class Student { ... } 
public class Teacher { ... } 
public class School { 
public Student[] students; 
public Teacher[] teachers; 
} 

组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段.   has - a 语义包含  is - a 语义是一个大类  中的一员

三.多态

1.向上转型

此时 bird2 是一个父类 (Animal) 的引用, 指向一个子类 (Bird) 的实例. 这种写法称为 向上转型.

向上转型发生的时机:

直接赋值

Animal bird2 = new 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 的实例

2.动态绑定

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("谷子"); } 
} // 执行结果 
//我是一只小动物 
//圆圆正在吃谷子 
//我是一只小鸟 
//扁扁正在吃谷子

此时, 我们发现:

animal1 和 animal2 虽然都是 Animal 类型的引用, 但是 animal1 指向 Animal 类型的实例, animal2 指向 Bird 类型的实例. 针对 animal1 和 animal2 分别调用 eat 方法,

发现 animal1.eat() 实际调用了父类的方法, 而 animal2.eat() 实际调用了子类的方法

因此, 在 Java 中, 调用某个类的方法, 究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) , 要看究竟这个引 用指向的是父类对象还是子类对象. 这个过程是程序运行时决定的(而不是编译期), 因此称为动态绑定

3.方法重写

子类实现父类的同名方法, 并且参数的类型和个数完全相同, 这种情况称为 覆写/重写/覆盖(Override).

1. 重写和重载完全不一样. 不要混淆 

2. 普通方法可以重写, static 修饰的静态方法不能重写

3. 重写中子类的方法的访问权限不能低于父类的方法访问权限.

4. 重写的方法返回值类型不一定和父类的方法相同(但是建议最好写成相同, 特殊情况除外).类实现父类的同名方法, 并且参数的类型和个数完全相同, 这种情况称为 覆写/重写/覆盖(Override)

方法权限示例: 将子类的 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.b.Bird中的eat(java.lang.String)无法覆盖com.b.Animal中的 eat(java.lang.String) 正在尝试分配更低的访问权限; 以前为public 
// Bird.java public class Bird extends Animal { 
@Override 
private void eat(String food) { ... 
} 
}

另外, 针对重写的方法, 可以使用 @Override 注解来显式指定.  有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发 现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写. 我们推荐在代码中进行重写方法时显式加上 @Override 注解. 关于注解的详细内容, 我们会在后面的章节再详细介绍.

小结: 重载和重写的区别.

4.多态

通过概念引入, 我们可以引入多态概念

代码示例: 打印多种形状
class 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("♣"); 
 } 
} 
/我是分割线// 
// Test.java 
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 对应的实例相关), 这种行为就称为 多态

使用多态的好处是什么?

1) 类调用者对类的使用成本进一步降低. 封装是让类的调用者不需要知道类的实现细节.

多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可

2) 能够降低代码的 "圈复杂度", 避免使用大量的 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(); 
 } 
}

什么叫 "圈复杂度" ?

圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如果有很 多的条件分支或者循环语句, 就认为理解起来更复杂. 因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 "圈复杂度". 如果一 个方法的圈复杂度太高, 就需要考虑重构 . 不同公司对于代码的圈复杂度的规范不一样. 一般不会超过 10 .

3) 可扩展能力更强

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(); // 执行结果, 抛出异常 Exception in thread "main" java.lang.ClassCastException: Cat cannot be cast to Bird at Test.main(Test.java:35) 

animal 本质上引用的是一个 Cat 对象  , 是不能转成 Bird 对象的. 运行时就会抛出异常.

所以, 为了让向下转型更安全, 我们可以先判定一下看看 animal 本质上是不是一个 Bird 实例, 再来转换

Animal animal = new Cat("小猫"); 
if (animal instanceof Bird) { 
Bird bird = (Bird)animal;
bird.fly(); 
} 

instanceof 可以判定一个引用是否是某个类的实例. 如果是, 则返回 true. 这时再进行向下转型就比较安全了.

5.super关键字

可以使用 super 关键字在子类内部调用父类方法

super 表示获取到父类实例的引用. 涉及到两种常见用法.

1) 使用了 super 来调用父类的构造器

public Bird(String name) { 
    super(name); 
}

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 功能有些相似, 但是还是要注意其中的区别:

 6.易出现错误

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.func() 0 

构造 D 对象的同时, 会调用 B 的构造方法. B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的 func 此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0.

结论: 用尽量简单的方式使对象进入可工作状态", 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发 动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题.

无论是哪种编程语言, 多态的核心都是让调用者不必关注对象的具体类型. 这是降低用户使用成本的一种重要方式.

四.抽象类

 没有实际工作的方法, 我们可以把它设计成一个 抽象方法(abstract method), 包含抽象方法的类我们称为 抽象类(abstract class).

abstract class Shape { 
 abstract public void draw(); 
} 

在 draw 方法前加上 abstract 关键字, 表示这是一个抽象方法.

同时抽象方法没有方法体(没有 { }, 不能执行具体 代码). 对于包含抽象方法的类, 必须加上 abstract 关键字表示这是一个抽象类

注意事项
1) 抽象类不能直接实例化.  
Shape shape = new Shape(); 
// 编译出错
Error:(30, 23) java: Shape是抽象的; 无法实例化
2) 抽象方法不能是 private 的
abstract class Shape { 
 abstract private void draw(); 
} 
// 编译出错
Error:(4, 27) java: 非法的修饰符组合: abstract和private 

3) 抽象类中可以包含其他的非抽象方法, 也可以包含字段. 这个非抽象方法和普通方法的规则都是一样的, 可以被重写, 也可以被子类直接调用

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

抽象类的作用:被继承

抽象类本身不能被实例化, 要想使用, 只能创建该抽象类的子类. 然后让子类重写抽象类中的抽象方法.

既然普通的类也可以被继承呀, 普通的方法也可以被重写  为什么非得用抽象类和抽象方法呢?

确实如此. 但是使用抽象类相当于多了一重编译器的校验  使用抽象类的场景就如上面的代码, 实际工作不应该由父类完成, 而应由子类完成. 那么此时如果不小心误用成父类了, 使用普通类编译器是不会报错的. 但是父类是抽象类就会在实例化的时候提示错误, 让我们尽早发现问题.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值