文章目录
- 一 基础概念
- 1 有关Java
- 2 JVM / JDK / JRE
- 3 与C++的联系和区别
- 4 各类型数据占用空间大小
- 5 == 和 equals() 的区别、hashCode() 方法
- 6 包装类型
- 7 final 关键字
- 8 参数传递机制:值传递
- 9 String 的内存情况
- 10 访问修饰符
- 11 引用拷贝、浅拷贝与深拷贝
- 三 面向对象
- 1 面向过程与面向对象
- 2 构造方法
- 3 OOP特性:封装、继承、多态
- 4 重载和重写
- 5 继承:重名问题
- 6 继承:实现原理
- 7 继承的问题:破坏封装
- 8 类的扩展:接口
- 9 类的扩展:抽象类
- 10 类的扩展:内部类
- 7 代理模式
- 四 异常
- 1 异常分类
- 2 try-catch-finally
- 3 try-with-resources
- 五 文件与 I/O 流
- 1 transient 关键字
- 2 文件类型:文本文件和二进制文件
- 3 文件读写
- 4 Java I/O 流
- 4.1二进制文件 - 字节流 - byte
- 4.2 文本文件 - 字符流 - char
- 六 反射
- 1 动态语言
- 2 什么是反射
- 3 反射常用类与方法
- 4 DEMO
- 5 反射的应用:JDK动态代理
- 6 反射与泛型
- 七 泛型
- 1 概念和语法
- 2 泛型擦除
一 基础概念
1 有关Java
- Java是编译与解释并存的语言:由 Java 编写的程序需要先经过编译步骤,生成字节码文件(.class 文件,面向JVM而非特定系统的),这种字节码必须由 Java 解释器来解释执行
- 编译型语言会通过编译器将源代码一次性翻译成可被该平台执行的机器码。一般情况下,编译语言的执行速度比较快,开发效率比较低
- 解释型语言会通过解释器逐句的将代码解释为机器代码后再执行。解释型语言开发效率比较快,执行速度比较慢
2 JVM / JDK / JRE
包含范围从大到小:
JDK(Java development kit)
:功能齐全的 Java SDK。拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序JRE(Java runtime environment)
:Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是不能用于创建新程序JVM(Java virtual machine)
:运行 Java 字节码(.class格式文件)的虚拟机,针对不同系统有不同的实现。JVM是一种规范,满足规范的虚拟机都可称为JVM
3 与C++的联系和区别
- 都是面向对象的语言,支持封装、继承、多态
- Java 不提供指针来直接访问内存,程序内存更加安全
- Java 的类是单继承的(接口支持多重继承),C++ 支持多重继承
- Java 有自动内存管理垃圾回收机制
- C ++同时支持方法重载和操作符重载,但是 Java 只支持方法重载
4 各类型数据占用空间大小
- Java中比较特殊的是char类型占用2字节(16bit)
- char 本质上是占用两个字节的无符号整数,对应 Unicode 编号,用于表示对应字符
5 == 和 equals() 的区别、hashCode() 方法
- 对于基本数据类型,只能用 == 比较
- 对于引用数据类型, == 用于比较内存地址; equals() 如果未被重写,也是比较内存地址,重写后按照指定规则判断两个对象是否相等
- 重写
equals()
方法时,必须同时重写hashCode()
方法 - 两个对象的 hashCode 值相等并不代表两个对象就相等(哈希碰撞)。两个对象相等则 hashCode 必相等
- 两个对象的比较,首先比较 hashCode() 的返回值是否相等,如果不相等直接认为两个对象不相等,如果相等则继续调用 equals() 方法,返回 True 时视为两个对象相等
- 如果重写
equals()
时没有重写hashCode()
方法的话,可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等 hashCode()
存在的意义是,减少equals()
的调用,提高执行速度
6 包装类型
- 包装类型的比较必须用
equals()
- 基本数据类型存放在 Java 虚拟机栈中的局部变量表中;而包装类型属于对象类型,存在于堆中
- 包装类常量池技术
Byte,Short,Integer,Long
这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据Character
创建了数值在 [0,127] 范围的缓存数据Float,Double
没有实现常量池
- 装箱与拆箱:装箱其实就是调用了 包装类的valueOf()方法,拆箱其实就是调用了 xxxValue()方法
Integer i = 10; //装箱,等价于 Integer i = Integer.valueOf(10)
int n = i; //拆箱,int n = i.intValue()
7 final 关键字
- 被 final 关键字修饰的类不能被继承
- 修饰的方法不能被重写
- 修饰的变量是基本数据类型则值不能改变
- 修饰的变量是引用类型则不能再指向其他对象
8 参数传递机制:值传递
- Java只存在值传递,如果向方法传递引用类型,则在方法中产生引用类型的堆中的地址的拷贝
public class Person {private String name;// 省略构造函数、Getter&Setter方法
}public static void main(String[] args) {Person xiaoZhang = new Person("小张");Person xiaoLi = new Person("小李");swap(xiaoZhang, xiaoLi);System.out.println("xiaoZhang:" + xiaoZhang.getName());System.out.println("xiaoLi:" + xiaoLi.getName());
}public static void swap(Person person1, Person person2) {Person temp = person1;person1 = person2;person2 = temp;System.out.println("person1:" + person1.getName());System.out.println("person2:" + person2.getName());
}输出:
person1:小李
person2:小张 // 在swap方法中,调换了person1和person2的指向
xiaoZhang:小张
xiaoLi:小李 // 在主方法中,引用指向并未改变
- swap 方法的参数 person1 和 person2 只是拷贝的实参 xiaoZhang 和 xiaoLi 的地址
- 因此, person1 和 person2 的互换只是拷贝的两个地址的互换,不会影响到实参 xiaoZhang 和 xiaoLi
- 另附验证代码:
public class Main {public static void change(Person p) {//p = new Person("更改后"); // main打印结果是更改前p.name = "更改后"; // main打印结果是更改后,直接更改了输入内存地址里的内容}public static void main(String[] args) {Person p_in = new Person("更改前");change(p_in);System.out.println(p_in.name);}
}class Person {public String name;public Person(String name) {this.name = name;}
}
9 String 的内存情况
参考 JVM 博客
- String 类底层使用 char[] 存储
- String 不同实例化方式的内存对比
- 字符串拼接的内存情况
str_instance.intern()
将字符串 str 放到常量池中
10 访问修饰符
修饰符 | 范围 |
---|---|
public | 无限制 |
protected | 子类、当前包 |
default | 当前包 |
private | 仅限当前类 |
protected
由于其子类的可见性,多用于模板模式
11 引用拷贝、浅拷贝与深拷贝
- 引用拷贝:创建新的对象引用,指向原来的对象
- 浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象
- 深拷贝 :深拷贝会完全复制整个对象,包括这个对象所包含的内部对象
三 面向对象
1 面向过程与面向对象
- 面向过程 :面向过程性能比面向对象高。 因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix 等一般采用面向过程开发
- 面向对象 :面向对象易维护、易复用、易扩展。 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护
2 构造方法
- 类默认具有不带参数的构造方法,如果添加了类的构造方法(无论是否有参),Java 就不会再添加默认的无参数的构造方法了
- 无论是否用到,要把无参的构造方法写出来
- 原因:如果子类构造方法没有显式地调用 super 构造器,则默认调用
super()
- 子类调用父类的构造方法,使用
super(...)
,且必须在子类构造方法的首行调用 - 构造方法不能重写,但是可以重载
- 通过子类构造器创建对象时,一定会直接或间接地调用父类的构造器,直到调用了 Object 类的构造器,且父类的构造方法先于子类执行
3 OOP特性:封装、继承、多态
- 封装:封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性
- 继承:继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有
- 多态:编译时类型和运行时类型不一致,具体表现为父类的引用指向子类的实例。引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;多态不能调用“只在子类存在但在父类不存在”的方法,尽管内存中加载了子类特有的属性和方法,想要调用需要向下转型。多态情况下,父类的方法称为虚(拟)方法,调用方法的过程称为动态绑定
4 重载和重写
- 重载:相同的方法名,不同的参数列表。可以发生在一个类中,也可以发生在父类和子类间。重载就是多个同名方法根据不同的传参来执行不同的逻辑处理。重载发生在编译时
- 重写:相同的方法名,相同的参数列表。发生在父类和子类间。本质上是子类覆盖了父类的方法。重写发生在运行时,具体要求:
- 抛出异常的类型更小或相等
- 访问权限更大或相等
- 返回值的类型更小或相等,如果方法的返回类型是 void 和基本数据类型,则返回值重写时不可修改。但是如果方法的返回值是引用类型,重写时可以返回该引用类型的子类
5 继承:重名问题
- 如果重名的属性或方法是 private 修饰的,则父类和子类互不影响
class Base {public static String s = "base_static_field";public String m = "base_nonstatic_field";public static void staticTest() {System.out.println("base: " + s);}
}class Child extends Base {public static String s = "child_static_field";public String m = "child_nonstatic_field";public static void staticTest() {System.out.println("child: " + s);}
}public class Verify {public static void main(String[] args) {Child child = new Child(); // 该对象具有两个声明为 public 的属性 mBase base = child;// =====子类通过类型转换得到的父类========System.out.println(Base.s);Base.staticTest();System.out.println(base.m);// ==========子类=============System.out.println(Child.s);Child.staticTest();System.out.println(child.m);}
}输出结果:
base_static_field
base: base_static_field
base_nonstatic_fieldchild_static_field
child: child_static_field
child_nonstatic_field
6 继承:实现原理
- 类加载时需要执行的代码(执行顺序和代码顺序有关)
- 静态代码块
- 静态变量声明时的赋值
- 创建对象时需要执行的代码(粗体执行顺序和代码顺序有关,构造方法最后执行)
- 非静态代码块
- 非静态变量声明时的赋值
- 构造方法
- 寻找要执行的实例方法时,从对象的实际类型开始查找,找不到的时候从父类递归查找
- 例如调用
base.func()
时,过程如下base
的实际类型为Child
,在Child
中找不到func()
,从父类Base
查找Base
包含func()
,开始执行func()
调用test()
,在Child
中找到test()
,执行并返回到func()
func()
返回,执行完成
- 例如调用
- 构造场景如下
public class Verify {public static void main(String[] args) {// 创建子类对象Child child = new Child();// 强转为父类,并调用方法Base base = child;base.func();}
}class Base {static {System.out.println("base static block");}{System.out.println("base nonstatic block");}public Base() {System.out.println("base constructor");}public void func() {test();}public void test() {System.out.println("base method call");}
}class Child extends Base {static {System.out.println("child static block");}{System.out.println("child nonstatic block");}public Child() {System.out.println("child constructor");}@Overridepublic void test() {System.out.println("child method call");}
}输出结果:
base static block // 1.父类加载
child static block // 2.子类加载
base nonstatic block
base constructor // 3.执行父类实例化代码
child nonstatic block
child constructor // 4.执行子类实例化代码,至此完成子类对象创建child method call // 调用的是子类而非父类的test()
7 继承的问题:破坏封装
- 使用继承时,必须严格符合 is-a 的关系,否则会造成混乱
- 例如,“鸟类”作为父类,拥有“可以飞”的方法,“企鹅”作为子类就是一种不好的实现
- 子类继承父类时,如果不知道父类的实现细节,就无法正确地进行扩展
- 解决方法
- 使用
final
避免继承- 被
final
修饰的方法,父类拥有随意修改方法内部实现的自由 - 被
final
修饰的类,由于无法被继承,其实现是自由的
- 被
- 使用组合 + 接口,将父类对象作为子类的一个属性,同时保证父类和子类的一致性
interface Add {void plusOne();
}class Base implements Add {@Overridevoid plusOne() {// ...}
}class Child implements Add {Base b;@Overridevoid plusOne() {b.plusOne();// 方法扩展...}
}
8 类的扩展:接口
- 接口声明了一组能力,但它自己没有对这些能力做出实现,仅是一个约定
- 继承要求子类和父类间存在 is-a 的关系,而接口关注的是具有某种能力 is-able-to
- 在某些情况下,代码只关注一个类是否具有某个能力,而不关注具体类型(面向接口编程),可以实现高效的代码复用
- 接口的一个重要功能是降低了耦合
- 使用接口的程序依赖于接口本身,而非实现接口的具体类型
- 可以灵活替换接口实现而不影响接口使用
- 接口可以有变量,必须且默认被
public static final
修饰 - 接口可以多继承,类也可以实现多个接口
- 和类一样,接口可以使用
instanceof
判断某个类是否实现了某个接口 - JDK8和9的接口增强:接口内可以定义静态方法和默认方法
- 引入默认方法的目的是,方便给现有的类增加新方法
- 将新增方法设置为
default
,现有的接口实现类无需实现默认方法
public interface Demo {// 普通方法声明void hello();// 静态方法public static void hola() {System.out.println("hola");}// 默认方法default void hi() {System.out.println("hi");}
}
9 类的扩展:抽象类
- 接口声明能力;抽象类提供默认实现,实现全部或部分方法,方便子类实现接口
- 一个接口经常有一个对应的抽象类
- 抽象类可以定义实例变量,而接口不可以
- 抽象类有构造方法,但是不能被实例化
10 类的扩展:内部类
- 内部类只是编辑器的概念,对JVM而言,每个内部类都会被编译成一个独立的类,生成独立的字节码文件
- 四种内部类
-
静态内部类
- 与外部类关系密切,且不依赖于外部类实例
- 对于外部类,只能访问外部类的静态属性和方法(包含
private
) - 静态内部类的方法可以设置为静态,也可以非静态,但都只能访问外部类的静态属性和方法
-
成员内部类
- 对于外部类,可以访问外部类的静态和非静态的成员和方法(包含
private
) - 成员内部类对象总是和一个外部类对象相连
- 成员内部类中不能定义静态的属性和方法
- 需要先创建外部类对象,再创建内部类对象
- 一种应用场景是,外部类的方法返回值是某个接口,使用
private
的内部类实现该接口,并在方法中返回,此时的接口实现对外完全隐藏
- 对于外部类,可以访问外部类的静态和非静态的成员和方法(包含
-
方法内部类
- 应用场景和成员内部类相似,可以由成员内部类代替(如果某个类只在某个方法中被使用,用方法内部类的封装性更好)
- 方法可以是静态的,也可以是非静态的,对方法内部类的区别在于,能否访问外部类的非静态属性
- 方法内部类可以访问
final
修饰的方法参数和局部变量
-
匿名内部类
public class Verify {public static void main(String[] args) {// ===1.静态内部类===// 实例化静态内部类,调用其非静态方法,仅能访问外部类的静态属性Outer.StaticInner staticInner = new Outer.StaticInner();staticInner.accessOuterStaticField();// 如果暴露的方法是静态方法,则无需创建对象即可执行Outer.StaticInner.accessOuterStaticFieldByStaticMethod();// ===2.成员内部类===// 实例化非静态内部类,并调用其方法,访问外部类的所有属性Outer.NonStaticInner nonStaticInner = new Outer().new NonStaticInner();nonStaticInner.accessAllOuterField();// ===3.方法内部类===new Outer().getInstance("param");}
}class Outer {// 外部静态属性private static String outerStaticField = "outerStaticField";// 外部非静态属性private String outerNonStaticField = "outerNonStaticField";// 外部静态方法private static void outerStaticMethod() {System.out.println("outerStaticMethod");}// 外部非静态方法private void outerNonStaticMethod() {System.out.println("outerNonStaticMethod");}// 1.静态内部类static class StaticInner {public void accessOuterStaticField() {System.out.println("===StaticInner-NonStaticMethod===");outerStaticMethod(); // 访问外部静态方法System.out.println(outerStaticField); // 访问外部静态属性}public static void accessOuterStaticFieldByStaticMethod() {System.out.println("===StaticInner-StaticMethod===");outerStaticMethod(); // 访问外部静态方法System.out.println(outerStaticField); // 访问外部静态属性}}// 2.非静态内部类class NonStaticInner {public void accessAllOuterField() {System.out.println("===NonStaticInner===");outerStaticMethod(); // 访问外部静态方法outerNonStaticMethod(); // 访问外部非静态方法System.out.println(outerStaticField); // 访问外部静态属性System.out.println(outerNonStaticField); // 访问外部非静态属性}}// 3.方法内部类SomeAbility getInstance(final String param) {final String localParam = "localParam";class MethodInner implements SomeAbility {// 接口实现...public MethodInner() {System.out.println("===MethodInner===");System.out.println(param);System.out.println(localParam);}}return new MethodInner();}
}
7 代理模式
静态代理
- 静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。
- 静态代理的实现步骤:定义一个接口及其实现类; 创建一个代理类同样实现这个接口,将目标对象注入进代理类(使其成为代理类的成员变量);然后在代理类的对应方法调用目标类中的对应方法。
- 静态代理中,对目标对象的每个方法的增强都是手动完成的,非常不灵活(比如接口一旦新增加方法,目标对象和代理对象都要进行修改且麻烦(需要对每个目标类都单独写一个代理类)。
JDK动态代理(反射的应用)
- 动态代理在运行时动态生成类字节码,并加载到 JVM 中的。
- 动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。“一个代理类,完成全部的代理功能”。
使用实例:
- 接口与接口的实现
interface Human{String getBelief();void eat(String food);
}class SuperMan implements Human{@Overridepublic String getBelief() {return "I believe I can fly!";}@Overridepublic void eat(String food) {System.out.println("我喜欢吃" + food);}
}
- 动态代理类:动态代理对象调用原生方法的时候,最终实际上调用到的是 invoke() 方法,然后 invoke() 方法调用了被代理对象的原生方法。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;public class DebugInvocationHandler implements InvocationHandler {// 代理类中的真实对象private final Object target;public DebugInvocationHandler(Object target) {this.target = target;}/*** 动态代理的核心部分!!!* proxy :动态生成的代理类* method : 与代理类对象调用的方法相对应* args : 当前 method 方法的参数**/public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {//额外操作...Object result = method.invoke(self.target, args); //调用的是method的invoke方法//额外操作...return result;}
}
- 获取代理对象的工厂类:输入需要被代理的对象,输出其代理。即:根据对象实例建立代理实例。
public class JdkProxyFactory {public static Object getProxy(Object target) {return Proxy.newProxyInstance(target.getClass().getClassLoader(), // 目标类的类加载target.getClass().getInterfaces(), // 代理需要实现的接口,可指定多个new DebugInvocationHandler(target) // 代理对象对应的自定义 InvocationHandler);}
}
- 实际使用:一个代理,到处使用
public static void main(String[] args) {//被代理的类型1SuperMan superMan = new SuperMan();Human proxyInstance = (Human) ProxyFactory.getProxyInstance(superMan); // 强转为对应接口的类型// invoke() 方法: 当我们的动态代理对象调用原生方法的时候,最终实际上调用到的是 invoke() 方法,然后 invoke() 方法代替我们去调用了被代理对象的原生方法。String belief = proxyInstance.getBelief();proxyInstance.eat("四川麻辣烫");//被代理的类型2NikeClothFactory nikeClothFactory = new NikeClothFactory();ClothFactory proxyClothFactory = (ClothFactory); ProxyFactory.getProxyInstance(nikeClothFactory);proxyClothFactory.produceCloth();}
四 异常
1 异常分类
- 在编译过程中,如果受检查异常没有被
catch/throw
处理的话,就无法通过编译 - 未受检异常表示编程时的逻辑错误,应该修改逻辑而非进行异常处理;受检异常表示程序本身没有问题,由于一些不可预测的错误导致的异常
2 try-catch-finally
try
- 用于捕获异常
- 其后可接零个或多个
catch
块,如果没有catch
,则必须有finally
catch
- 用于处理
try
捕获到的异常
- 用于处理
finally
- 无论是否捕获或处理异常,
finally
都会被执行 - 当在
try
或catch
中返回时,finally
将在方法返回之前被执行,但不会改变返回值
static int finallyTest() {int i = 1;try {return i;} finally {i++;} } // 返回结果是1而不是2 // 因为执行到 try 内的返回语句时,会将返回值保存在临时变量中,然后执行 finally // 返回的是临时变量而非修改后的值
- 只要
finally
语句中有返回语句,finally
语句的内容将被执行(而不是try
或catch
的返回语句),这样会掩盖原返回值或异常,避免在finally
中返回或抛出异常
- 无论是否捕获或处理异常,
3 try-with-resources
- 在 try 后增加括号,在其中创建资源对象(任何实现
java.lang.AutoCloseable
或者java.io.Closeable
的对象),不必显式关闭
try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {int b;while ((b = bin.read()) != -1) {bout.write(b);}}catch (IOException e) {e.printStackTrace();}
五 文件与 I/O 流
1 transient 关键字
- 作用是阻止实例中那些用此关键字修饰的变量被序列化
- 当对象被反序列化时,被
transient
修饰的变量值不会被持久化和恢复 transient
只能修饰变量,不能修饰类和方法transient
修饰的变量,在反序列化后变量值将会被置成类型的默认值- 静态变量因为不属于任何对象,所以无论有没有
transient
关键字修饰,均不会被序列化
- 当对象被反序列化时,被
2 文件类型:文本文件和二进制文件
- 文本文件的每个二进制字节都是某个可打印字符的一部分,都可以用最基本的文本编辑器查看和编辑
- 二进制文件中,每个字节不一定表示字符,可能表示颜色、字体等,如果用基本的文本编辑器直接查看一般是乱码
3 文件读写
- 操作系统和硬盘交互时,一般按块批量传输,以均摊延时开销
- 一般读写文件需要两次复制
- 读文件:硬盘 -> 操作系统内核 -> 应用程序内存
- 操作系统在操作文件时,一般有打开和关闭的概念
- 打开文件会在操作系统内核建立一个有关该文件的内存结构,这个结构一般通过一个整数索引引用,这个索引被称为文件描述符
- 操作系统能同时打开的文件数是有限的,所以在不使用文件时要关闭
4 Java I/O 流
- Java 具有很多面向流的方法,接受的参数和返回的方法都是流接口对象(面向接口编程,类似容器类的协作体系
- 一些实际上不是 IO 的数据也或目的地的对象也可以转换为流,以使用流的方法,例如字节数组可以包装为
ByteArrayInputStream
和ByteArrayOutputStream
- 序列化和反序列化
- 序列化就是将内存中的 Java 对象持久地保存到一个流中,反序列化就是从流中恢复 Java 对象到内存
- 作用:对象的持久化;网络远程调用
- 通过接口
Serialzable
标记对象是可序列化的 - 常见的序列化方式:JSON、ProtoBuf
4.1二进制文件 - 字节流 - byte
InputStream/OutputStream
基类,抽象类
abstract class InputStream ... {// 阻塞直到读取下一个字节public abstract int read() throws IOException// 阻塞直到读取若干字节,将读到的字节放入参数数组b中public abstract int read(byte[] b) throws IOExceptionpublic abstract int read(byte[] b, int off, int len) throws IOException
}abstract class OutputStream ... {// 向流中写入一个字节(仅写入b最低的8位)public abstract void write(int b) throws IOException// 批量写入public abstract void write(byte[] b) throws IOExceptionpublic abstract void write(byte[] b, int off, int len) throws IOException// 将缓冲数据写入,默认实现只是将数据传递给操作系统,实际写入时机由操作系统决定public void flush() throws IOException
}
FileInputStream/FileOutputStream
输入源和输出目标是文件
class FileInputStream extends InputStream ... {// 构造方法public FileInputStream(File file) throws FileNotFoundExceptionpublic FileInputStream(String name) throws FileNotFoundException
}class FileOutputStream extends OutputStream ... {// 构造方法public FileOutputStream(File file, boolean append) throws FileNotFoundExceptionpublic FileOutputStream(String name) throws FileNotFoundException
}
ByteArrayInputStream/ByteArrayOutputStream
输入源和输出目标是字节数组
class ByteArrayInputStream extends InputStream ... {// 构造方法,适配器模式,将字节数组包装为一个输入流public ByteArrayInputStream(byte[] buf)public ByteArrayInputStream(byte[] buf, int offset, int length)
}class ByteArrayOutputStream extends OutputStream ... {// 构造方法,底层数组大小会根据数据内容动态扩展public ByteArrayOutputStream()public ByteArrayOutputStream(int size)// 将数据转换为其它类型public synchronized byte[] toByteArray()public synchronized String toString()public synchronized String toString(String charsetName)// 写入另一个输出流public synchronized void writeTo(OutputStream out) throws IOException
}
// 使用 ByteArrayOutputStream 读文件
public static void main(String[] args) {InputStream input = new FileInputStream("hello.txt");try {ByteArrayOutputStream output = new ByteArrayOutputStream();byte[] buf = new byte[1024];int bytesRead = 0;while ((bytesRead = input.read(buf)) != -1) { // 从文件输入流读取数据output.write(buf, 0, bytesRead); // 将读到的数据写入字节数组输出流}String data = output.toString("UTF-8"); // 将字节数组输出流转换为字符串} finally {input.close();}
}
DataInputStream/DataOutputStream
以非字节为单位进行读写- 是装饰器基类
FilterInputStream/FilterOutputStream
的子类,构造时均需要传入InputStream/OutputStream
对象
- 是装饰器基类
BufferedInputStream/BufferedOutputStream
添加缓冲区- 使用
FileInputStream/FileOutputStream
时应该总是使用缓冲区包装:InputStream input = new BufferedInputStream(new FileInputStream("hello.txt"));
- 使用
4.2 文本文件 - 字符流 - char
- 对于文本文件,字节流没有编码的概念,并且不能按行处理
- 字符流是按
char
读取的:对于绝大部分字符,一个字符对应一个char
;对于增补集的字符,需要两个char
表示 Reader/Writer
基类,抽象类InputStreamWriter/OutputStreamWriter
适配器类,将InputStream/OutputStream
转换为Reader/Writer
public static void main(String[] args) {// 输出字节流转换为字符流Writer writer = new OutputStreamWriter(new FileOutputStream("hello.txt"), "GB2312");try {writer.write("hello")} finally {writer.close();}
}
FileReader/FileWriter
不能指定编码类型,只能使用默认编码(如果需要指定编码,使用InputStreamWriter/OutputStreamWriter
)CharArrayReader/CharArrayWriter
输入源和输出目标是字符数组StringReader/StringWriter
本质同上,因为字符串底层是字符数组BufferedReader/BufferedWriter
装饰类,为Reader/Writer
提供缓冲
六 反射
1 动态语言
- 动态语言指运行时可以改变其结构的语言:例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化
- 反射机制使得Java成为了“准动态语言”:加载完类之后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息
2 什么是反射
- 在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性,并且能改变它的属性
- 反射机制允许程序在运行时取得任何一个已知名称接口、类或实例的内部信息,包括包括其修饰符、属性、方法、接口等,并可于运行时改变属性值或调用方法
- 反射并不是优先选项
- 反射容易出现运行时错误,因为编译器无法协助类型检查
- 反射的性能更低
- 如果能用接口实现同样目的,优先选择接口
3 反射常用类与方法
java.lang.Class
: 代表一个类- 获取 Class 实例不能使用
new
,而应该通过类名.class
或实例.getClass()
获得
- 获取 Class 实例不能使用
- 对应关系
Class类 | |
---|---|
Class实例 | 实例运行时类 |
实例 |
public static void main(String[] args) {// 同一个类的不同实例,对应的 Class 实例 是同一个Person p1 = new Person("1号");Person p2 = new Person("2号");Class c1 = p1.getClass();Class c2 = p2.getClass();Class c3 = Person.class;System.out.println(c1 == c2); // trueSystem.out.println(c1 == c3); // true// 数组的元素类型与维度(一维数组,二维数组等)一样,对应的 Class 实例 是同一个Class c4 = new String[10].getClass();Class c5 = new String[8].getClass();Class c6 = new int[10].getClass();Class c7 = String[].class;System.out.println(c4 == c5); // trueSystem.out.println(c4 == c6); // falseSystem.out.println(c4 == c7); // true}
java.lang.reflect.Method
: 代表类的方法java.lang.reflect.Field
: 代表类的属性java.lang.reflect.Constructor
: 代表类的构造器
4 DEMO
package com.ys.reflex;
public class Person {// 私有属性private String name = "Tom";// 公有属性public int age = 18;// 构造方法public Person() {}// 私有方法private void say(){System.out.println("private say()...");}// 公有方法public void work(){System.out.println("public work()...");}
}
- 获取
Class
对象的三种方式
// 1.通过对象调用 getClass() 方法来获取Person p1 = new Person();Class c1 = p1.getClass();// 2.直接通过 类名.class 的方式得到,该方法最为安全可靠,程序性能更高Class c2 = Person.class;// 3.通过 Class 对象的 forName() 静态方法来获取,但可能抛出 ClassNotFoundException 异常Class c3 = Class.forName("com.ys.reflex.Person");
- Class 类具有如下的方法:
getName()
:获得类的完整名字getFields()
:获得类的public
类型的属性getDeclaredFields()
:获得类的所有属性,包括private
声明的和继承父类的属性getMethods()
:获得类的public
类型的方法getDeclaredMethods()
:获得类的所有方法,包括private
声明的和继承父类的方法getMethod(String name, Class[] parameterTypes)
:获得类的特定方法,name 参数指定方法的名字,parameterTypes 参数指定方法的参数类型getConstructors()
:获得类的public类型的构造方法getConstructor(Class[] parameterTypes)
:获得类的特定构造方法,parameterTypes 参数指定构造方法的参数类型newInstance()
:通过类的无参构造方法创建这个类的一个对象- 运行时类必须提供空参的构造器;
- 空参的构造器的访问权限足够
- 写代码时总是要提供一个
public
的空参构造器,原因:- 便于通过反射,创建运行时类的对象
- 便于子类继承此运行时类时,默认调用
super()
时,保证父类有此构造器
public void test2() throws Exception{Class clazz = Person.class; // 获取Class对象// 1.通过反射,创建Person类的对象Constructor cons = clazz.getConstructor(String.class,int.class); // 获取指定构造器Object obj = cons.newInstance("Tom", 12); // 创建对象Person p = (Person) obj;// 2.通过反射,调用对象指定的属性、方法// 访问并修改属性Field age = clazz.getDeclaredField("age");age.set(p, 10);// 调用方法Method show = clazz.getDeclaredMethod("show");show.invoke(p); // 调用私有构造器Constructor cons1 = clazz.getDeclaredConstructor(String.class);cons1.setAccessible(true);Person p1 = (Person) cons1.newInstance("Jerry");// 访问并修改私有属性Field name = clazz.getDeclaredField("name");name.setAccessible(true);name.set(p1, "HanMeimei");// 调用私有方法Method showNation = clazz.getDeclaredMethod("showNation", String.class);showNation.setAccessible(true);String nation = (String) showNation.invoke(p1, "中国");// 调用静态私有方法Method showDesc = clazz.getDeclaredMethod("showDesc");showDesc.setAccessible(true);Object returnVal = showDesc.invoke(null);}
5 反射的应用:JDK动态代理
- 动态代理详细参考
6 反射与泛型
- 在
Class
对象中包含泛型信息
public class MyGeneticClass<K extends Comparable<K>, V> {K key;V value;List<String> list;public V test(List<? extends Number> numbers) {return null;}public static void main(String[] args) throws Exception {Class<MyGeneticClass> c = MyGeneticClass.class;// 获取类定义的泛型和边界for (TypeVariable t : c.getTypeParameters()) {System.out.println("genetic defined by class: " + t.getName() +" extends " + Arrays.toString(t.getBounds()));}// 属性的泛型for (Field field : c.getDeclaredFields()) {System.out.println("field: " + field.getGenericType());}// 方法泛型参数Method method = c.getDeclaredMethod("test", new Class[]{List.class});System.out.println("method parameter: " + Arrays.toString(method.getGenericParameterTypes()));System.out.println("method return: " + method.getGenericReturnType());}
}
七 泛型
1 概念和语法
- 泛型的本质是参数化类型,即数据类型被指定为一个参数
- 在指定类的泛型为某个参数时,和类的实例有关,因此静态方法不能使用类定义的泛型
- 如果允许使用,则对于每种实例化类型,都有一份对应的静态属性和方法(但从类型擦除的角度,这些实例化的类型属于一个类),这违反了静态的原则
- 静态属性和方法中不能使用类的泛型,但静态方法可以自定义泛型,成为泛型方法
- 泛型方法并不是“使用”了泛型的方法,而是“定义”了新泛型的方法
- 泛型方法可以用
static
修饰
// 类定义(多个)泛型
class GeneticBase<K, V> {// 非静态属性可以使用类定义的泛型private K key;private V value;// 普通方法,可以使用类定义的泛型public void commonMethod() {// 可以使用 K, V}// 非静态泛型方法,可以使用类定义的泛型public <T> void nonstaticGeneticMethod() {// 可以使用 K, V, T}// 静态泛型方法,不能使用类定义的泛型(泛型类是和实例相关的)public static <E> void staticGeneticMethod(E e) {// 可以使用 E}
}
- 基本类型不能用于实例化泛型参数,如有需要选择包装类
- 如果实例化泛型类时,没有指定具体的类型,则认为此泛型的类型为
Object
- 继承泛型类时,只需在
extends
后的父类后指明泛型类型即可public class SubOrder extends Order<String>
- 如果不指明,则当前类仍然沿用泛型
public class ArrayList<E> extends AbstractList<E>
- 不能直接创建泛型对象和数组,因为在编译时
T
不是一个具体的类,无法通过编译- 创建泛型类:通过反射
- 创建泛型数组:一般而言泛型容器可以满足需求,如果实在需要数组形式,同样需要反射
2 泛型擦除
- Java 的泛型是伪泛型,在运行期间,所有的泛型信息都会被擦掉(想象运行时的代码将泛型括号划去,容易理解很多问题)
- 将类型参数擦除,替换为
Object
,并进行必要的强制类型转换 - 从泛型擦除的角度理解:类 A 是类 B 的父类,
G<A>
和G<B>
二者不具备子父类关系;而A<G>
是B<G>
的父类
// 由于泛型擦除,不能定义如下的重载方法
public void test(MyGeneticClass<Integer> g);
public void test(MyGeneticClass<String> g);// 另一种现象
MyGenetic<Integer> i = new MyGenetic(1);
MyGenetic<String> s = new MyGenetic("a");
i.getClass() == MyGenetic.class; // true
s.getClass() == MyGenetic.class; // true// 不支持如下写法
i instanceof MyGenetic<Integer>// 同样不支持如下写法
class Base implements Comparable<Base> {...}
class Child extends Base implements Comparable<Child> {...} // 错误,接口不能被实现多次
// ===想改变子类的比较方法,只能@Override===
class Child extends Base {@Overridepublic int compareTo(Base o) {if (!o instance of Child) {throw new IllegalArgumentException();}Child c = (Child) o;// ...}
}