1 包
包 (package) 是组织类的一种方式. 使用包的主要目的是保证类的唯一性.
例如 , 你在代码中写了一个 Test 类. 然后你的同事也可能写一个 Test 类。 如果出现两个同名的类, 就会冲突, 导致代码不能编译通过。而如果在不同包写同一个类就不会发生这种情况,这也是为什么会出现包。
1.1 导入包中的类
Java 中已经提供了很多现成的类供我们使用。例如:
import java.util.Date;
public class TestDemo {
public static void main(String[] args) {
Date date = new Date();
System.out.println(date.getTime());
}
}
这里调用的Date类就是Java中提供给我们的现成的类,作用是得到一个毫秒级的时间戳,我们可以直接用来创建对象并调用相应的方法。这里最上面的导入是在创建对象时系统自动帮我们导入的,我们也可以自己来导入。
💫举个栗子:
public class TestDemo {
public static void main(String[] args) {
java.util.Date date = new java.util.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 TestDemo {
public static void main(String[] args) {
Date date = new Date();
System.out.println(date.getTime());
}
}
1.2 静态导入
使用 import static 可以导入包中的静态的方法和字段。
import static java.lang.System.*;
public class Test {
public static void main(String[] args) {
out.println("hello");
}
}
这里用了静态导入后输出前面的System就可以省略了,静态导入可以使代码书写更简单,但是不常用。
1.3 创建多级包
基本规则
- 在文件的最上方加上一个 package 语句指定该代码在哪个包中.
包名需要尽量指定成唯一的名字 , 通常会用公司的域名的颠倒形式(例如 com.bit.demo1 ).- 包名要和代码路径相匹配. 例如创建 com.bit.demo1 的包, 那么会存在一个对应的路径 com/bit/demo1 来存储代码.
- 如果一个类没有 package 语句, 则该类被放到一个默认包中.
步骤: 1:右键 src -> 新建 -> 包 2:输入多级包名,用.隔开 3:多级包生成
注意 :要想生成这种形式需要在结构设置中关掉一个设置
1.4 包的权限访问控制
类中的权限访问控制符有 public 和 private. private 中的成员只能被类的内部使用。 如果某个成员不包含 public 和 private 关键字, 此时这个成员可以在包内部的其他类使用, 但是不能在包外部的类使用。
🔑举个栗子:
Demo2类访问了同包的Demo1类中的包访问权限的变量value,可以正常输出
Test与Demo1为不同包,访问value就会报错。
1.5 常见系统包
- java.lang:系统常用基础类(String、Object),此包从JDK1.1后自动导入。
- java.lang.reflect:java 反射编程包;
- java.net:进行网络编程开发包。
- java.sql:进行数据库开发的支持包。
- java.util:是java提供的工具程序包。
(集合类等) 非常重要 - java.io:I/O编程开发包。
2 继承
2.1 背景
代码中创建的类, 主要是为了抽象现实中的一些事物(包含属性和方法)。有的时候客观事物之间就存在一些关联关系,那么在表示成类和对象的时候也会存在一定的关联。 例如, 设计一个类表示动物
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类继承Animal类就可以达到简化代码和代码重用的作用。
2.2 语法规则
基本语法:
class 子类 extends 父类 { }
- 使用
extends 指定父类. - Java 中
一个子类只能继承一个父类 (而C++/Python等语言支持多继承). - 子类会继承父类的所有 public 的字段和方法.
- 对于父类的 private 的字段和方法, 子类中是无法访问的.
- 子类的实例中, 也包含着父类的实例. 可以
使用 super 关键字得到父类实例的引用
💫举个栗子:
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(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();
}
}
在Bird和Cat类中虽然没有name成员变量和eat方法,但是当他们继承了Animal后就自然具备了父类的成员变量和方法,可以通过引用直接调用。
这里如果把name用private修饰那么就会出现问题,因为private修饰的变量只能在本类中才能访问。
2.3 protected关键字
刚才我们发现, 如果把字段设为 private, 子类不能访问。但是设成 public, 又违背了我们 “封装” 的初衷(任何位置的类都能访问public类)。 两全其美的办法就是protected 关键字。 对于类的 子类 和 同一个包的其他类 来说, protected 修饰的字段和方法是可以访问的 对于其他类的调用者来说, protected 修饰的字段和方法是不能访问的
小结: Java 中对于字段和方法共有四种访问权限 private: 类内部能访问, 类外部不能访问。 默认(也叫包访问权限) : 类内部能访问, 同一个包中的类可以访问, 其他类不能访问。 protected: 类内部能访问, 子类和同一个包中的类可以访问, 其他类不能访问。 public : 类内部和类的调用者都能访问。
2.4 复杂继承关系
刚才举的栗子是一级继承,事实上生活中的继承关系可以很复杂,比如
这样就是多层继承,我们并不希望类之间的继承层次太复杂。 一般我们不希望出现超过三层的继承关系. 如果继承层次太多, 就需要考虑对代码进行重构了。
2.5 final关键字
之前我们知道final修饰的常量是不能修改的(也就是常量),final同样也能用来修饰类,表示类不能被继承。
🐾举个栗子:
🔥小知识: 我们平时是用的 String 字符串类, 就是用 final 修饰的, 不能被继承。
3 多态
3.1 向上转型
在刚才的例子中, 我们写了形如下面的代码:
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("谷子");
}
}
🔑方法返回
public class Test {
public static void main(String[] args) {
Animal animal = findMyAnimal();
}
public static Animal findMyAnimal() {
Bird bird = new Bird("圆圆");
return bird;
}
}
3.2 动态绑定
这里我们通过一个栗子来看:
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(name);
}
}
class Bird extends Animal {
public Bird(String name) {
super(name);
}
public void fly() {
System.out.println(this.name + "正在飞 ︿( ̄︶ ̄)︿");
}
public void eat(String food) {
System.out.println(this.name + "疯狂吃" + food);
}
}
public class Test {
public static void main(String[] args) {
Animal bird = new Bird("花花");
bird.eat("食物");
}
}
我们发现我创建了一个Animal的引用指向一个Bird实例,当调用eat方法时,调用的不是Animal中的eat方法,而是我在Bird类中重写的方法。
在 Java 中, 调用某个类的方法, 究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) , 要看究竟这个引用指向的是父类对象还是子类对象. 这个过程是程序运行时决定的(而不是编译期), 因此称为动态绑定。
3.3 方法重写
针对刚才的 eat 方法来说: 子类实现父类的同名方法, 并且参数的类型和个数完全相同, 这种情况称为 覆写/重写/覆盖(Override).
关于重写的注意事项
- 重写和重载完全不一样. 不要混淆.
- 普通方法可以重写,
static 修饰的静态方法不能重写 .(静态方法在类加载时就加载到内存中了,在整个运行过程中不变,非静态方法在对象实例化才单独申请内存空间,所以可以重写. ) 重写中子类的方法的访问权限不能低于父类的方法访问权限. - 重写的方法返回值类型不一定和父类的方法相同(但是建议最好写成相同, 特殊情况除外).
针对重写的方法, 可以使用 @Override 注解来显式指定.
重写方法快捷键 ctrl+o
小知识: 重载和重写的区别 1.重写参数列表和返回值必须与被重写的方法一样 ,重载参数列表必须有不同的参数列表,返回值类型可以不同 2.重写方法访问修饰符的限制一定要大于被重写的方法 ,重载可以有不同的访问修饰符 3.重写方法不能抛出比被重写方法更加宽泛的异常,重载可以抛出不同的异常
3.4 如何理解多态
有了面的向上转型, 动态绑定, 方法重写之后, 我们就可以使用 多态(polypeptide) 的形式来设计程序了.我们可以写一些只关注父类的代码, 就能够同时兼容各种子类的情况
💫举个栗子:
class Printf{
String name = "ant";
void printf(){
System.out.println("sss");
}
}
class Rect extends Printf{
@Override
public void printf() {
System.out.println("△");
}
}
class Cur extends Printf{
@Override
public void printf() {
System.out.println("○");
}
}
class Flower extends Printf{
@Override
public void printf() {
System.out.println("?");
}
}
public class Test {
public static void printY(Printf pintf){
pintf.printf();
}
public static void main(String[] args) {
printY(new Cur());
printY(new Rect());
printY(new Flower());
}
}
利用多态可以实现一个函数根据子类重写方法的不同产生不同的结果。 多态顾名思义, 就是 "一个引用, 能表现出多种不同形态"
使用多态的好处是什么?
- 类调用者对类的使用成本进一步降低.
多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可. - 能够降低代码的 “圈复杂度”, 避免使用大量的 if - else
- 可扩展能力更强.如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低
3.5 向下转型
有时候父类中没有子类中有的方法而又想通过父类引用调用子类方法时就会用到向下转型。
🔑举个栗子:
Animal animal = new Bird("圆圆")
Bird bird = (Bird)animal;
bird.fly();
这样就可以执行fly方法了,但是向下转换是有风险了,比如
Animal animal = new Cat("小猫");
Bird bird = (Bird)animal;
bird.fly();
这样就会报错,animal 本质上引用的是一个 Cat 对象, 是不能转成 Bird 对象的. 运行时就会抛出异常.所以, 为了让向下转型更安全, 我们可以先判定一下看看 animal 本质上是不是一个 Bird 实例, 再来转换
Animal animal = new Cat("小猫");
if (animal instanceof Bird) {
Bird bird = (Bird)animal;
bird.fly();
}
instanceof 可以判定一个引用是否是某个类的实例. 如果是, 则返回 true. 这时再进行向下转型就比较安全了.
3.6 super关键字
前面的代码中由于使用了重写机制, 调用到的是子类的方法. 如果需要在子类内部调用父类方法怎么办? 可以使用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);
}
}
小知识 :super和this的区别 1.this访问本类的属性,如果本类没有则从父类查找。super访问父类的属性。 2.this访问本类的方法,如果本类没有则从父类查找。super访问父类的方法。 3.this调用本类构造方法,必须放在构造方法的首行。super调用父类构造,必须放在子类构造方法首行。 4.this表示当前方法,super不能表示当前对象。
3.7 在构造方法中调用重写方法的坑
🔑举个栗子:
class B {
public B() {
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
作者水平有限,若文章有任何问题欢迎私聊或留言,希望和大家一起学习进步!!! 创作不易,再次希望大家👍支持下,谢谢大家🙏
|