文章目录
1、封装
我们写代码的时候经常会涉及两种角色: 类的实现者和类的调用者.
封装的本质就是让类的调用者不必太多的了解类的实现者是如何实现类的, 只要知道如何使用类就行了.这样就降低了类使用者的学习和使用成本, 从而降低了复杂程度.
1.1 private实现封装
private/ public 这两个关键字表示 “访问权限控制” .
- 被 public 修饰的成员变量或者成员方法, 可以直接被类的调用者使用.
- 被 private 修饰的成员变量或者成员方法, 不能被类的调用者使用.
1.2 getter和setter方法
当我们使用 private 来修饰字段的时候, 就无法直接使用这个字段了.
此时我们就需要把这些private字段封装到public的方法中,这样就更好的保护了数据。
2、继承
我们写一个类,就是为了抽象现实中的一些事物,这些事物之间是可能存在联系的,在表示成类和对象的时候也会存在一定的关联。
例如设计一个类表示动物:
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 语义).
此时我们就可以让 Cat 和 Bird 分别继承 Animal 类, 来达到代码重用的效果.
此时, Animal 这样被继承的类, 我们称为 父类 , 基类 或 超类;对于像 Cat 和 Bird 这样的类, 我们称为 子类, 派生类;子类继承父类的字段和方法, 以达到代码重用的效果.
对于上面的代码, 可以使用继承进行改进. 此时我们让 Cat 和 Bird 继承自 Animal 类, 那么 Cat 在定义的时候就不必再写 name 字段和 eat 方法.
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.eat("猫粮");
Bird bird = new Bird("圆圆");
bird.fly();
}
}
extends 英文原意指 “扩展”. 而我们所写的类的继承, 也可以理解成基于父类进行代码上的 “扩展”.
例如我们写的 Bird 类, 就是在 Animal 的基础上扩展出了 fly 方法.
访问权限符:
3、多态
多态: 一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在程序运行期间才能决定。因为在程序运行时才确定具体的类,不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
3.1 向上转型
在刚才的例子中, 我们写了形如下面的代码
Bird bird = new Bird("圆圆");
这个代码也可以写成这个样子
Bird bird = new Bird("圆圆");
Animal bird2 = bird;
// 或者写成下面的方式
Animal bird2 = new Bird("圆圆");
此时 bird2 是一个父类 (Animal) 的引用, 指向一个子类 (Bird) 的实例. 这种写法称为 向上转型.
适用场景: 当不需要面对子类类型时,通过提高扩展性,或者使用父类的功能就能完成相应的操作。
3.2 向下转型
向上转型是子类对象转成父类对象, 向下转型就是父类对象转成子类对象.
Animal animal = new Bird("圆圆");
Bird bird = (Bird)animal;
bird.fly();
这就叫向下转型,将父类对象转成子类对象。
适用场景: 当要使用子类特有功能时。
3.3 动态绑定
当子类和父类中出现同名方法的时候, 再去调用会出现什么情况呢?
对前面的代码稍加修改, 给 Bird 类也加上同名的 eat 方法, 并且在两个 eat 中分别加上不同的日志.
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);
}
}
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 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.4 多态存在的三个必要条件
- 继承
- 重写
- 父类引用指向子类对象
3.5 多态的实现方式
- 重写
- 接口
- 抽象类
4、泛型
泛型就是指在类定义的时候不会设置类中属性或者方法中的具体类型,而是类在使用的时候再进行定义。
类型擦除机制: 泛型信息只存在于代码编译阶段,在进⼊入 JVM 之前,与泛型相关的信息会被擦除掉,专业术语叫做类型擦除。
5、反射
5.1 认识反射机制
反射指的是对象的反向处理理操作,既然是反向处理理。我们先来观察一下"正"的操作。在默认情况下,必须要先导入一个包,而后才能产生类的实例化对象。
例子:正常处理
import java.util.Date;
public class Test {
public static void main(String[] args) {
Date date = new Date() ;
}
}
以上是我们正常的关于对象的处理理流程:根据包名.类名找到类
所谓的"反"指的是根据对象来取得对象的来源信息,而这个"反"的操作核心的处理理就在于Object类的一个方法:
取得Class对象:
public final native Class<?> getClass();
该方法返回的是一个Class类对象,这个Class描述的就是类。
例子:调用getClass()方法
import java.util.Date;
public class Test {
public static void main(String[] args) {
Date date = new Date() ;
System.out.println(date.getClass());
}
}
此时通过对象取得了了对象的来源,这就是"反"的本质。
在反射的世界里面,看重的不不再是一个对象,而是对象身后的组成(类、构造、普通、成员等)
5.2 3种实例化对象
Class类是描述整个类的概念,也是整个反射的操作源头,在使用Class类的时候需要关注的依然是这个类的对象。而这个类的对象的产生模式一共有三种:
- 任何类的实例化对象可以通过Object类中的getClass()方法取得Class类对象。
- “类.class”:直接根据某个具体的类来取得Class类的实例化对象。
- 使⽤用Class类提供的方法:public static Class<?> forName(String className) throws
ClassNotFoundException
import java.util.Date;
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
Date date = new Date();
Class<?> cla2 = date.getClass();
System.out.println(cla2.getName());
Class<?> cla = Class.forName("java.util.Date");
System.out.println(cla.getName());
Class<?> cla1 = java.util.Date.class;
System.out.println(cla1.getName());
}
}
在以上给出的三个方法中我们可以发现,除了第一种方法会产生Date类的实例化对象之外,其他的两种都不会产生Date类的实例例化对象。于是取得了Class类对象有一个最直接的好处:可通过反射实例化对象,在Class类中定义有如下方法:
public T newInstance() throws InstantiationException, IllegalAccessException
反射实例化对象:
public class Main {
public static void main(String[] args) throws ClassNotFoundException,
IllegalAccessException, InstantiationException {
Class<?> cla = Class.forName("java.util.Date");
//实例化对象 等价于 new java.util.Date();
Object obj = cla.newInstance();
System.out.println(obj);
}
}
现在发现除了关键字new之外,对于对象的实例化模式有了第二种做法,通过反射进行。
5.3 反射VS工厂模式
工厂设计模式曾经给过原则:如果是自己编写的接口,要想取得本接口的实例例化对象,最好使用工厂类来设计。但是也需要知道传统工厂设计所带来的问题。
传统工厂类:
public interface Fruit {
public void get();
}
public class Apple implements Fruit{
@Override
public void get() {
System.out.println("Apple");
}
}
public class Banana implements Fruit {
@Override
public void get() {
System.out.println("Banana");
}
}
public class FruitFactory {
public static Fruit getInstance(String type) {
if ("apple".equals(type)) {
return new Apple();
} else if ("banana".equals(type)) {
return new Banana();
} else {
return null;
}
}
}
public class Main {
public static void main(String[] args) {
Fruit fruit = FruitFactory.getInstance("apple");
fruit.get();
}
}
(问题就在于new)。每增加一个接口的子类就需要修改工厂类。
传统工厂类增加接口子类:
public class Orange implements Fruit {
@Override
public void get() {
System.out.println("Orange");
}
}
public class FruitFactory {
public static Fruit getInstance(String type) {
if ("apple".equals(type)) {
return new Apple();
} else if ("banana".equals(type)) {
return new Banana();
} else if ("orange".equals(type)) {
return new Orange();
} else {
return null;
}
}
}
如果要想解决关键字new带来的问题,最好的做法就是通过反射来完成处理,因为Class类可以使用newInstance()实例化对象,同时Class.forName()能够接收类名称。
修改程序:
public interface Fruit {
public void get();
}
public class Apple implements Fruit{
@Override
public void get() {
System.out.println("Apple");
}
}
public class Banana implements Fruit {
@Override
public void get() {
System.out.println("Banana");
}
}
public class Orange implements Fruit {
@Override
public void get() {
System.out.println("Orange");
}
}
public class FruitFactory {
public static Fruit getInstance(String className) {
Fruit fruit = null;
try {
fruit = (Fruit) Class.forName(className).newInstance();
} catch (InstantiationException | IllegalAccessException
| ClassNotFoundException e) {
e.printStackTrace();
}
return fruit;
}
}
public class Main {
public static void main(String[] args) {
Fruit fruit = FruitFactory.getInstance("orange");
fruit.get();
}
}
引入反射后,每当新增接口子类,无需去修改工厂类代码就可以很方便的进行接口子类扩容。以上这种工厂类代码我们称之为简单工厂模式
5.4 ClassLoader类加载器
Class类描述的是整个类的信息,在Class类中提供的forName()方法,这个方法根据ClassPath配置的路径进行类的加载,如果说现在你的类的加载路路径可能是网络、文件,这个时候就必须实现类加载器,也就是ClassLoader类的主要作用。
5.4.1 认识ClassLoader
例子:
// ⾃自定义类,这个类⼀一定在CLASSPATH中
class Member{}
public class TestDemo {
public static void main(String[] args) {
Class<?> cls = Member.class ;
System.out.println(cls.getClassLoader()) ;
System.out.println(cls.getClassLoader().getParent()) ;
System.out.println(cls.getClassLoader().getParent().getParent());
}
}
//运行结果:
//sun.misc.Launcher$AppClassLoader@58644d46
//sun.misc.Launcher$ExtClassLoader@6a38e57f
//null
此时出现了两个类加载器:ExtClassLoader(扩展类加载器)、AppClassLoader(应用程序类加载器)。
类加载过程?
- 加载:根据查找路径找到相应的class文件,然后导入。类的加载方式分为隐式加载和显示加载两种。
- 隐式加载指的是程序在使用new关键词创建对象时,会隐式的调用类的加载器把对应的类加载到jvm中。
- 显示加载指的是通过直接调用class.forName()方法来把所需的类加载到jvm中。
-
检查:检查夹加载的class文件的正确性。
-
准备;给类中的静态变量分配内存空间。
-
解析:虚拟机将常量池中的符号引用替换成直接引用的过程。符号引用就理解为一个标示,而在直接引用直接指向内存中的地址。
-
初始化:对静态变量和静态代码块执行初始化工作。
什么是类加载器?
负责动态加载Java类到Java虚拟机的内存空间中。
Bootstrap(启动类加载器): 是虚拟机自身的一部分,其他类加载器都独立于JVM外部,并且都继承于启动类加载器。负责把<Java_HOME>\lib目录下核心类库的jar包加载到内存中
ExtClassLoader(扩展类加载器): 它负责将<Java_HOME>\lib\ext目录下的jar包加载到内存中。
AppClassLoader(应用程序类加载器): 负责加载用户类路径(ClassPath)上指定的类库到内存。
5.4.2 双亲委派模型
上图展示的类加载器之间的这种层次关系,就称为类加载器的双亲委派模型。
双亲委派模型的工作流程是:如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此。因此,所有的加载请求都应当传送到顶层的BootStrap加载器中,只有当父加载器器反馈无法完成这个加载请求时(在自己搜索范围中没有找到此类),子加载器器才会尝试自己去加载。