本文主要是我最近复习 Java 基础原理过程中写的 Java 基础学习总结. Java 的知识点其实非常多, 并且有些知识点比较难以理解, 有时候我们自以为理解了某些内容, 其实可能只是停留在表面上, 没有理解其底层实现原理.
纸上得来终觉浅, 绝知此事要躬行. 笔者之前对每部分的内容 对做了比较深入的学习以及代码实现, 基本上比较全面地讲述了每一个 Java 基础知识点, 当然可能有些遗漏和错误, 还请读者指正.
Java 基础学习总结
每部分内容会重点写一些常见知识点, 方便复习和记忆, 但是并不是全部内容, 详细的内容请参见具体的文章地址.
面向对象三大特性
继承: 一般类只能单继承, 内部类实现多继承, 接口可以多继承
封装: 访问权限控制 public> protected> 包 > private 内部类也是一种封装
多态: 编译时多态, 体现在向上转型和向下转型, 通过引用类型判断调用哪个方法 (静态分派).
运行时多态, 体现在同名函数通过不同参数实现多种方法 (动态分派).
基本数据类型
基本类型位数, 自动装箱, 常量池
例如 byte 类型是 1byte 也就是 8 位, 可以表示的数字是 - 128 到 127, 因为还有一个 0, 加起来一共是 256, 也就是 2 的八次方.
32 位和 64 位机器的 int 是 4 个字节也就是 32 位, char 是 1 个字节就是 8 位, float 是 4 个字节, double 是 8 个字节, long 是 8 个字节.
所以它们占有字节数是相同的, 这样的话两个版本才可以更好地兼容.(应该)
基本数据类型的包装类只在数字范围 - 128 到 127 中用到常量池, 会自动拆箱装箱, 其余数字范围的包装类则会新建实例
String 及包装类
String 类型是 final 类型, 在堆中分配空间后内存地址不可变.
底层是 final 修饰的 char[] 数组, 数组的内存地址同样不可变.
但实际上可以通过修改 char[n] = 'a'来进行修改, 不会改变 String 实例的内存值, 不过在 jdk 中, 用户无法直接获取 char[], 也没有方法能操作该数组.
所以 String 类型的不可变实际上也是理论上的不可变. 所以我们在分配 String 对象以后, 如果将其 = "abc", 那也只是改变了引用的指向, 实际上没有改变原来的对象.
StringBuffer 和 StringBuilder 底层是可变的 char[] 数组, 继承父类 AbstractStringBuilder 的各种成员和方法, 实际上的操作都是由父类方法来完成的.
final 关键字
final 修饰基本数据类型保证不可变
final 修饰引用保证引用不能指向别的对象, 否则会报错.
final 修饰类, 类的实例分配空间后地址不可变, 子类不能重写所有父类方法. 因此在 cglib 动态代理中, 不能为一个类的 final 修饰的函数做代理, 因为 cglib 要将被代理的类设置为父类, 然后再生成字节码.
final 修饰方法, 子类不能重写该方法.
抽象类和接口
1 抽象类可以有方法实现.
抽象类可以有非 final 成员变量.
抽象方法要用 abstract 修饰.
抽象类可以有构造方法, 但是只能由子类进行实例化.
2 接口可以用 extends 加多个接口实现多继承.
接口只能有 public final 类型的成员变量.
接口只能有抽象方法, 不能有方法体,
接口不能实例化, 但是可以作为引用类型.
代码块和加载顺序
假设该类是第一次进行实例化. 那么有如下加载顺序
静态总是比非静态优先, 从早到晚的顺序是:
1 静态代码块 和 静态成员变量的顺序根据代码位置前后来决定.
2 代码块和成员变量的顺序也根据代码位置来决定
3 最后才调用构造方法构造方法
包, 内部类, 外部类
1 Java 项目一般从 src 目录开始有 com...A.java 这样的目录结构. 这就是包结构. 所以一般编译后的结构是跟包结构一模一样的, 这样的结构保证了 import 时能找到正确的 class 引用包访问权限就是指同包下的类可见.
import 一般加上全路径, 并且使用.* 时只包含当前目录的所有类文件, 不包括子目录.
2 外部类只有 public 和 default 两种修饰, 要么全局可访问, 要么包内可访问.
3 内部类可以有全部访问权限, 因为它的概念就是一个成员变量, 所以访问权限设置与一般的成员变量相同.
非静态内部类是外部类的一个成员变量, 只跟外部类的实例有关.
静态内部类是独立于外部类存在的一个类, 与外部类实例无关, 可以通过外部类. 内部类直接获取 Class 类型.
异常
1 异常体系的最上层是 Throwable 类
子类有 Error 和 Exception
Exception 的子类又有 RuntimeException 和其他具体的可检查异常.
2 Error 是 jvm 完全无法处理的系统错误, 只能终止运行.
运行时异常指的是编译正确但运行错误的异常, 如数组越界异常, 一般是人为失误导致的, 这种异常不用 try catch, 而是需要程序员自己检查.
可检查异常一般是 jvm 处理不了的一些异常, 但是又经常会发生, 比如 Ioexception,Sqlexception 等, 是外部实现带来的异常.
3 多线程的异常流程是独立的, 互不影响.
大型模块的子模块异常一般需要重新封装成外部异常再次抛出, 否则只能看到最外层异常信息, 难以进行调试.
日志框架是异常报告的最好帮手, log4j,slf4j 中, 在工作中必不可少.
泛型
Java 中的泛型是伪泛型, 只在编译期生效, 运行期自动进行泛型擦除, 将泛型替换为实际上传入的类型.
泛型类用 class A {
} 这样的形式表示, 里面的方法和成员变量都可以用 T 来表示类型. 泛型接口也是类似的, 不过泛型类实现泛型接口时可以选择注入实际类型或者是继续使用泛型.
泛型方法可以自带泛型比如 void E go();
泛型可以使用? 通配符进行泛化 Object 可以接受任何类型
也可以使用 这种方式进行上下边界的限制.
Class 类和 Object 类
Java 反射的基础是 Class 类, 该类封装所有其他类的类型信息, 并且在每个类加载后在堆区生成每个类的一个 Class < 类名 > 实例, 用于该类的实例化.
Java 中可以通过多种方式获取 Class 类型, 比如 A.class,new A().getClass() 方法以及 Class.forName("com.?.?.A") 方法.
Object 是所有类的父类, 有着自己的一些私有方法, 以及被所有类继承的 9 大方法.
有人讨论 Object 和 Class 类型谁先加载谁后加载, 因为每个类都要继承 Object, 但是又得先被加载到堆区, 事实上, 这个问题在 JVM 初始化时就解决了, 没必要多想.
javac 和 java
javac 是编译一个 java 文件的基本命令, 通过不同参数可以完成各种配置, 比如导入其他类, 指定编译路径等.
java 是执行一个 java 文件的基本命令, 通过参数配置可以以不同方式执行一个 java 程序或者是一个 jar 包.
javap 是一个 class 文件的反编译程序, 可以获取 class 文件的反编译结果, 甚至是 jvm 执行程序的每一步代码实现.
反射
Java 反射包 reflection 提供对 Class,Method,field,constructor 等信息的封装类型.
通过这些 API 可以轻易获得一个类的各种信息并且可以进行实例化, 方法调用等.
类中的 private 参数可以通过 setaccessible 方法强制获取.
反射的作用可谓是博大精深, JDK 动态代理生成代理类的字节码后, 首先把这个类通过 defineclass 定义成一个类, 然后用 class.for(name) 会把该类加载到 jvm, 之后我们就可以通过, A.class.GetMethod() 获取其方法, 然后通过 invoke 调用其方法, 在调用这个方法时, 实际上会通过被代理类的引用再去调用原方法.
枚举类
枚举类继承 Enum 并且每个枚举类的实例都是唯一的.
枚举类可以用于封装一组常量, 取值从这组常量中取, 比如一周的七天, 一年的十二个月.
枚举类的底层实现其实是语法糖, 每个实例可以被转化成内部类. 并且使用静态代码块进行初始化, 同时保证内部成员变量不可变.
序列化
序列化的类要实现 serializable 接口
transient 修饰符可以保证某个成员变量不被序列化
readObject 和 writeOject 来实现实例的写入和读取.
待更新.
事实上, 一些拥有数组变量的类都会把数组设为 transient 修饰, 这样的话不会对整个数组进行序列化, 而是利用专门的方法将有数据的数组范围进行序列化, 以便节省空间.
动态代理
jdk 自带的动态代理可以代理一个已经实现接口的类.
cglib 代理可以代理一个普通的类.
动态代理的基本实现原理都是通过字节码框架动态生成字节码, 并且在用 defineclass 加载类后, 获取代理类的实例.
一般需要实现一个代理处理器, 用来处理被代理类的前置操作和后置操作. 在 JDK 动态代理中, 这个类叫做 invocationHandler.
JDK 动态代理首先获取被代理类的方法, 并且只获取在接口中声明的方法, 生成代理类的字节码后, 首先把这个类通过 defineclass 定义成一个类, 然后把该类加载到 jvm, 之后我们就可以通过, A.class.GetMethod() 获取其方法, 然后通过 invoke 调用其方法, 在调用这个方法时, 实际上会通过被代理类的引用再去调用原方法.
而对于 cglib 动态代理, 一般会把被代理类设为代理类的父类, 然后获取被代理类中所有非 final 的方法, 通过 asm 字节码框架生成代理类的字节码, 这个代理类很神奇, 他会保留原来的方法以及代理后的方法, 通过方法数组的形式保存.
cglib 的动态代理需要实现一个 enhancer 和一个 interceptor, 在 interceptor 中配置我们需要的代理内容. 如果没有配置 interceptor, 那么代理类会调用被代理类自己的方法, 如果配置了 interceptor, 则会使用代理类修饰过的方法.
多线程
这里先不讲 juc 包里的多线程类. juc 相关内容会在 Java 并发专题讲解.
线程的实现可以通过继承 Thread 类和实现 Runable 接口
也可以使用线程池. callable 配合 future 可以实现线程中的数据获取.
Java 中的线程有 7 种状态, new runable running blocked waiting time_waiting terminate
blocked 是线程等待其他线程锁释放.
waiting 是 wait 以后线程无限等待其他线程使用 notify 唤醒
time_wating 是有限时间地等待被唤醒, 也可能是 sleep 固定时间.
Thread 的 join 是实例方法, 比如 a.join(b), 则说明 a 线程要等 b 线程运行完才会运行.
o.wait 方法会让持有该对象 o 的线程释放锁并且进入阻塞状态, notify 则是持有 o 锁对象的线程通知其他等待锁的线程获取锁. notify 方法并不会释放锁. 注意这两个方法都只能在 synchronized 同步方法或同步块里使用.
synchronized 方法底层使用系统调用的 mutex 锁, 开销较大, jvm 会为每个锁对象维护一个等待队列, 让等待该对象锁的线程在这个队列中等待. 当线程获取不到锁时则让线程阻塞, 而其他检查 notify 以后则会通知任意一个线程, 所以这个锁时非公平锁.
Thread.sleep(),Thread.interrupt() 等方法都是类方法, 表示当前调用该方法的线程的操作.
一个线程实例连续 start 两次会抛异常, 这是因为线程 start 后会设置标识, 如果再次 start 则判断为错误.
IO 流
IO 流也是 Java 中比较重要的一块, Java 中主要有字节流, 字符流, 文件等. 其中文件也是通过流的方式打开, 读取和写入的.
IO 流的很多接口都使用了装饰者模式, 即将原类型通过传入装饰类构造函数的方式, 增强原类型, 以此获得像带有缓冲区的字节流, 或者将字节流封装成字符流等等, 其中需要注意的是编码问题, 后者打印出来的结果可能是乱码哦.
IO 流与网络编程息息相关, 一个 socket 接入后, 我们可以获取它的输入流和输出流, 以获取 TCP 数据包的内容, 并且可以往数据报里写入内容, 因为 TCP 协议也是按照流的方式进行传输的, 实际上 TCP 会将这些数据进行分包处理, 并且通过差错检验, 超时重传, 滑动窗口协议等方式, 保证了 TCP 数据包的高效和可靠传输.
网络编程
承接 IO 流的内容
IO 流与网络编程息息相关, 一个 socket 接入后, 我们可以获取它的输入流和输出流, 以获取 TCP 数据包的内容, 并且可以往数据报里写入内容, 因为 TCP 协议也是按照流的方式进行传输的, 实际上 TCP 会将这些数据进行分包处理, 并且通过差错检验, 超时重传, 滑动窗口协议等方式, 保证了 TCP 数据包的高效和可靠传输.
除了使用 socket 来获取 TCP 数据包外, 还可以使用 UDP 的 DatagramPacket 来封装 UDP 数据包, 因为 UDP 数据包的大小是确定的, 所以不是使用流方式处理, 而是需要事先定义他的长度, 源端口和目标端口等信息.
为了方便网络编程, Java 提供了一系列类型来支持网络编程的 API, 比如 URL 类, InetAddress 类等.
后续文章会带来 NIO 相关的内容, 敬请期待.
Java8
接口中的默认方法, 接口终于可以有方法实现了, 使用注解即可标识出默认方法.
lambda 表达式实现了函数式编程, 通过注解可以声明一个函数式接口, 该接口中只能有一个方法, 这个方法正是使用 lambda 表达式时会调用到的接口.
Option 类实现了非空检验
新的日期 API
各种 API 的更新, 包括 chm,hashmap 的实现等
Stream 流概念, 实现了集合类的流式访问, 可以基于此使用 map 和 reduce 并行计算.
来源: http://www.bubuko.com/infodetail-2990214.html