Java 基础
1. 数据类型
1.1 基本类型
- byte/8
- char/16
- short/16
- int/32
- float/32
- long/64
- double/64
- boolean/~
精度依次: dlfiscb
boolean 只有两个值: true,false, 可以使用 1 bit 来存储, 但是具体大小没有明确规定. JVM 会在编译时期将 boolean 类型的数据转换为 int, 使用 1 来表示 true,0 表示 false
1.2 包装类型
基本类型都有对应的包装类型, 基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成
1.3 缓存池
new Integer(123) 每次都会新建一个对象;
Integer.valueOf(123) 会使用缓存池中的对象, 多次调用会取得同一个对象的引用.
valueOf() 方法的实现比较简单, 就是先判断值是否在缓存池中, 如果在的话就直接返回缓存池的内容. 在 Java 8 中, Integer 缓存池的大小默认为 -128~127(超出范围直接 == 比较会 false). 编译器会在自动装箱过程调用 valueOf() 方法, 因此多个 Integer 实例使用自动装箱来创建并且值相同, 那么就会引用相同的对象.
2. String
2.1 概况
String 被声明为 final, 因此它不可被继承. 在 Java 8 中, String 内部使用 char 数组存储数据. 在 Java 9 之后, String 类的实现改用 byte 数组存储.
2.2 不可变的好处
可以缓存 hash 值
因为 String 的 hash 值经常被使用, 例如 String 用做 HashMap 的 key. 不可变的特性可以使得 hash 值也不可变, 因此只需要进行一次计算.
String Pool 的需要
如果一个 String 对象已经被创建过了, 那么就会从 String Pool 中取得引用. 只有 String 是不可变的, 才可能使用 String Pool.
安全性
String 经常作为参数, String 不可变性可以保证参数不可变. 试想一下 map 作为参数的场景.
线程安全
String 不可变性天生具备线程安全, 可以在多个线程中安全地使用.
2.3 String str = new String("abc")创建了多少个对象
首先必须弄清楚创建对象的含义, 创建是什么时候创建的? 这段代码在运行期间会创建 2 个对象么? 毫无疑问不可能, 用 javap -c 反编译即可得到 JVM 执行的字节码内容:
很显然, new 只调用了一次, 也就是说只创建了一个对象.
而这道题目让人混淆的地方就是这里, 这段代码在运行期间确实只创建了一个对象, 即在堆上创建了 "abc" 对象. 而为什么大家都在说是 2 个对象呢, 这里面要澄清一个概念: 该段代码执行过程和类的加载过程是有区别的. 在类加载的过程中, 确实在运行时常量池中创建了一个 "abc" 对象, 而在代码执行过程中确实只创建了一个 String 对象. 因此, 这个问题如果换成 String str = new String("abc")涉及到几个 String 对象? 合理的解释是 2 个. 所以要弄清楚语境.
2.4 String, StringBuffer and StringBuilder
StringBuffer 线程安全(所有方法都加了 synchronized)
对于直接相加字符串, 效率很高, 因为在编译器便确定了它的值, 也就是说形如 "I"+"love"+"java"; 的字符串相加, 在编译期间便被优化成了 "Ilovejava". 这个可以用 javap -c 命令反编译生成的 class 文件进行验证.
对于间接相加(即包含字符串引用), 形如 s1+s2+s3; 效率要比直接相加低, 因为在编译器不会对引用变量进行优化.
String,StringBuilder,StringBuffer 三者的执行效率
StringBuilder> StringBuffer> String, 当然这个是相对的, 不一定在所有情况下都是这样. 比如 String str = "hello"+ "world" 的效率就比 StringBuilder st = new StringBuilder().append("hello").append("world")要高.
StringBuilder 代码
- public AbstractStringBuilder append(String str) {
- if (str == null) str = "null";
- int len = str.length();
- ensureCapacityInternal(count + len);
- str.getChars(0, len, value, count);
- count += len;
- return this;
- }
- private void ensureCapacityInternal(int minimumCapacity) {
- // overflow-conscious code
- if (minimumCapacity - value.length> 0)
- expandCapacity(minimumCapacity);
- }
- void expandCapacity(int minimumCapacity) {
- int newCapacity = value.length * 2 + 2;
- if (newCapacity - minimumCapacity <0)
- newCapacity = minimumCapacity;
- if (newCapacity < 0) {
- if (minimumCapacity < 0) // overflow
- throw new OutOfMemoryError();
- newCapacity = Integer.MAX_VALUE;
- }
- value = Arrays.copyOf(value, newCapacity);
- }
- public String toString() {
- // Create a copy, don't share the array
- return new String(value, 0, count);
- }
每次调用 toString 方法, 会重新 new 一个 String 出来(为了保证 String 的不可变). 使用不当 CPU 会一直忙于进行内存的分配工作, 会导致机器的 load 过高
String Pool
字符串常量池 (String Pool) 保存着所有字符串字面量(literal strings), 这些字面量在编译时期就确定. 不仅如此, 还可以使用 String 的 intern() 方法在运行过程中将字符串添加到 String Pool 中.
当一个字符串调用 intern() 方法时, 如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定), 那么就会返回 String Pool 中字符串的引用; 否则, 就会在 String Pool 中添加一个新的字符串, 并返回这个新字符串的引用.
- String s1 = new String("aaa");
- String s2 = new String("aaa");
- System.out.println(s1 == s2); // false
- String s3 = s1.intern();
- String s4 = s1.intern();
- System.out.println(s3 == s4); // true
在 Java 7 之前, String Pool 被放在运行时常量池中, 它属于永久代. 而在 Java 7,String Pool 被移到堆中. 这是因为永久代的空间有限, 在大量使用字符串的场景下会导致 OutOfMemoryError 错误.
3. 运算
3.1 参数传递
Java 的参数是以值传递的形式传入方法中, 而不是引用传递.
如果在方法中改变对象的字段值会改变原对象该字段值, 因为改变的是同一个地址指向的内容, 并非因为是引用传递
3.2 float 与 double
Java 不能隐式执行向下转型, 因为这会使得精度降低
// float f = 1.1;
3.3 隐式类型转换
因为字面量 1 是 int 类型, 它比 short 类型精度要高, 因此不能隐式地将 int 类型下转型为 short 类型.
- short s1 = 1;
- // s1 = s1 + 1;
但是使用 += 或者 ++ 运算符可以执行隐式类型转换.
s1 += 1; // s1++;
上面的语句相当于将 s1 + 1 的计算结果进行了向下转型:
- s1 = (short) (s1 + 1);
- 3.4 switch
从 Java 7 开始, 可以在 switch 条件判断语句中使用 String 对象.
switch 不支持 long, 是因为 switch 的设计初衷是对那些只有少数的几个值进行等值判断, 如果值过于复杂, 那么还是用 if 比较合适.
4. 继承
4.1 访问权限
4.2 抽象类与接口
抽象类
抽象类和普通类最大的区别是, 抽象类不能被实例化, 需要继承抽象类才能实例
化其子类.
接口
接口是抽象类的延伸, 在 Java 8 之前, 它可以看成是一个完全抽象的类, 也就是说它不能有任何的方法实现. 从 Java 8 开始, 接口也可以拥有默认的方法实现, 这是因为不支持默认方法的接口的维护成本太高了. 在 Java 8 之前, 如果一个接口想要添加新的方法, 那么要修改所有实现了该接口的类.
接口的成员 (字段 + 方法) 默认都是 public 的, 并且不允许定义为 private 或者 protected.
接口的字段默认都是 static 和 final 的.
比较
从设计层面上看, 抽象类提供了一种 IS-A 关系, 那么就必须满足里式替换原则, 即子类对象必须能够替换掉所有父类对象. 而接口更像是一种 LIKE-A 关系, 它只是提供一种方法实现契约, 并不要求接口和实现接口的类具有 IS-A 关系.
从使用上来看, 一个类可以实现多个接口, 但是不能继承多个抽象类.
接口的字段只能是 static 和 final 类型的, 而抽象类的字段没有这种限制.
接口的成员只能是 public 的, 而抽象类的成员可以有多种访问权限.
使用选择
使用接口:
需要让不相关的类都实现一个方法, 例如不相关的类都可以实现 Compareable 接口中的 compareTo() 方法;
需要使用多重继承.
使用抽象类:
需要在几个相关的类中共享代码(如模板方法设计模式).
需要能控制继承来的成员的访问权限, 而不是都为 public.
需要继承非静态和非常量字段.
在很多情况下, 接口优先于抽象类. 因为接口没有抽象类严格的类层次结构要求, 可以灵活地为一个类添加行为. 并且从 Java 8 开始, 接口也可以有默认的方法实现, 使得修改接口的成本也变的很低.
5. 重写与重载
5.1 重写(Override)
指子类实现了一个与父类在方法声明上完全相同的一个方法
5.2 重载(Overload)
存在于同一个类中, 指一个方法与已经存在的方法名称上相同, 但是参数类型, 个数, 顺序至少有一个不同.
应该注意的是, 返回值不同, 其它都相同不算是重载
涉及到重写时, 方法调用的优先级为(先参数, 后父子):
- this.show(O)
- super.show(O)
- this.show((super)O)
- super.show((super)O)
6. Object 通用方法
6.1 equals()
等价与相等
对于基本类型,== 判断两个值是否相等, 基本类型没有 equals() 方法.
对于引用类型,== 判断两个变量是否引用同一个对象, 而 equals() 判断引用的对象是否等价.
- Integer x = new Integer(1);
- Integer y = new Integer(1);
- System.out.println(x.equals(y)); // true
- System.out.println(x == y); // false
- 6.2 hashCode()
hashCode() 返回散列值, 而 equals() 是用来判断两个对象是否等价. 等价的两个对象散列值一定相同, 但是散列值相同的两个对象不一定等价.
在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法, 保证等价的两个对象散列值也相等. 否则在集合对象中用起来就比较坑
下面的代码中, 新建了两个等价的对象, 并将它们添加到 HashSet 中. 我们希望将这两个对象当成一样的, 只在集合中添加一个对象, 但是因为 EqualExample 没有实现 hasCode() 方法, 因此这两个对象的散列值是不同的, 最终导致集合添加了两个等价的对象.
- EqualExample e1 = new EqualExample(1, 1, 1);
- EqualExample e2 = new EqualExample(1, 1, 1);
- System.out.println(e1.equals(e2)); // true
- HashSet<EqualExample> set = new HashSet<>();
- set.add(e1);
- set.add(e2);
- System.out.println(set.size()); // 2
理想的散列函数应当具有均匀性, 即不相等的对象应当均匀分布到所有可能的散列值上. 这就要求了散列函数要把所有域的值都考虑进来. 可以将每个域都当成 R 进制的某一位, 然后组成一个 R 进制的整数. R 一般取 31, 因为它是一个奇素数, 如果是偶数的话, 当出现乘法溢出, 信息就会丢失, 因为与 2 相乘相当于向左移一位.
一个数与 31 相乘可以转换成移位和减法: 31*x == (x<<5)-x, 编译器会自动进行这个优化.
6.3 clone()
浅拷贝
拷贝对象和原始对象的引用类型引用同一个对象. 继承自 java.lang.Object 类的 clone()方法是浅拷贝.
深拷贝
拷贝对象和原始对象的引用类型引用不同对象.
clone() 的替代方案
使用 clone() 方法来拷贝一个对象即复杂又有风险, 它会抛出异常, 并且还需要类型转换.
Effective Java 书上讲到, 最好不要去使用 clone(), 可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象.
也可利用序列化来做深复制. 把对象写到流里的过程是序列化过程 (Serialization), 而把对象从流中读出来的过程则叫做反序列化过程(Deserialization). 应当指出的是, 写在流里的是对象的一个拷贝, 而原对象仍然存在于 JVM 里面. 在 Java 语言里深复制一个对象, 常常可以先使对象实现 Serializable 接口, 然后把对象(实际上只是对象的一个拷贝) 写到一个流里, 再从流里读出来, 便可以重建对象. 这样做的前提是对象以及对象内部所有引用到的对象都是可串行化的, 否则, 就需要仔细考察那些不可串行化的对象可否设成 transient, 从而将其排除在复制过程之外.
7. 关键字
7.1 static
存在继承的情况下, 初始化顺序为:
父类(静态变量, 静态语句块)
子类(静态变量, 静态语句块)
父类(实例变量, 普通语句块)
父类(构造函数)
子类(实例变量, 普通语句块)
子类(构造函数)
8. 反射
每个类都有一个 Class 对象, 包含了与类有关的信息. 当编译一个新类时, 会产生一个同名的 .class 文件, 该文件内容保存着 Class 对象.
类加载相当于 Class 对象的加载, 类在第一次使用时才动态加载到 JVM 中. 也可以使用 Class.forName("com.mysql.jdbc.Driver") 这种方式来控制类的加载, 该方法会返回一个 Class 对象.
反射可以提供运行时的类信息, 并且这个类可以在运行时才加载进来, 甚至在编译时期该类的 .class 不存在也可以加载进来.
反射的优点:
可扩展性: 应用程序可以利用全限定名创建可扩展对象的实例, 来使用来自外部的用户自定义类.
类浏览器和可视化开发环境: 一个类浏览器需要可以枚举类的成员. 可视化开发环境 (如 IDE) 可以从利用反射中可用的类型信息中受益, 以帮助程序员编写正确的代码.
调试器和测试工具: 调试器需要能够检查一个类里的私有成员. 测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义, 以确保一组测试中有较高的代码覆盖率.
反射的缺点:
尽管反射非常强大, 但也不能滥用. 如果一个功能可以不用反射完成, 那么最好就不用. 在我们使用反射技术时, 下面几条内容应该牢记于心.
性能开销: 反射涉及了动态类型的解析, 所以 JVM 无法对这些代码进行优化. 因此, 反射操作的效率要比那些非反射操作低得多. 我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射.
安全限制: 使用反射技术要求程序必须在一个没有安全限制的环境中运行. 如果一个程序必须在有安全限制的环境中运行, 如 Applet, 那么这就是个问题了.
内部暴露: 由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法), 所以使用反射可能会导致意料之外的副作用, 这可能导致代码功能失调并破坏可移植性. 反射代码破坏了抽象性, 因此当平台发生改变的时候, 代码的行为就有可能也随着变化.
9. 异常
Throwable 可以用来表示任何可以作为异常抛出的类, 分为两种: Error 和 Exception. 其中 Error 用来表示 JVM 无法处理的错误, Exception 分为两种:
受检异常: 需要用 try...catch... 语句捕获并进行处理, 并且可以从异常中恢复;
非受检异常 : 是程序运行时错误, 例如除 0 会引发 Arithmetic Exception, 此时程序崩溃并且无法恢复.
当程序捕获到了一个底层异常, 在处理部分选择了继续抛出一个更高级别的新异常给此方法的调用者. 这样异常的原因就会逐层传递. 这样, 位于高层的异常递归调用 getCause()方法, 就可以遍历各层的异常原因. 这就是 Java 异常链的原理. 异常链的实际应用很少, 发生异常时候逐层上抛不是个好注意, 上层拿到这些异常又能奈之何? 而且异常逐层上抛会消耗大量资源, 因为要保存一个完整的异常链信息
10. 注解
Java 注解是附加在代码中的一些元信息, 用于一些工具在编译, 运行时进行解析和使用, 起到说明, 配置的功能. 注解不会也不能影响代码的实际逻辑, 仅仅起到辅助性的作用.
注解的用处
生成文档. 这是最常见的, 也是 java 最早提供的注解. 常用的有 @param @return 等
跟踪代码依赖性, 实现替代配置文件功能. 比如 Dagger 2 依赖注入, 未来 java 开发, 将大量注解配置, 具有很大用处;
在编译时进行格式检查. 如 @override 放在方法前, 如果你这个方法并不是覆盖了超类方法, 则编译时就能检查出.
注解的原理
注解本质是一个继承了 Annotation 的特殊接口, 其具体实现类是 Java 运行时生成的动态代理类. 而我们通过反射获取注解时, 返回的是 Java 运行时生成的动态代理对象 $Proxy1. 通过代理对象调用自定义注解 (接口) 的方法, 会最终调用 AnnotationInvocationHandler 的 invoke 方法. 该方法会从 memberValues 这个 Map 中索引出对应的值. 而 memberValues 的来源是 Java 常量池.
11. 特性
11.1 java5
泛型 (Generics)
增强循环(Enhanced for Loop)
自动封箱拆箱(Autoboxing/Unboxing )
八大基本类型和它们的包装类能够自动的相互转换.
枚举(Typesafe Enums).
枚举是一种实现线程安全的单例模式的好方式
可变参数 (Varargs)
语法:(type... arguments)
可变参数本质仍然是用一个数组存储参数, 只是 java 隐藏了这一过程. 需要注意的是如果一个方法声明中含有可变参数, 那必须放在最后一个位置.
静态导入(Static Import)
注解(Annotations)
新的线程模型和并发库(java.util.concurrent).
11.2 java6
集合框架增强.
为了更好的支持双向访问集合. 添加了许多新的类和接口.
新的数组拷贝方法. Arrays.copyOf 和 Arrays.copyOfRange
Scripting. 可以让其他语言在 java 平台上运行. java6 包含了一个基于 Mozilla Rhino 实现的 JavaScript 脚本引擎.
支持 JDBC4.0 规范.
11.3 java7
二进制前缀 0b 或者 0B
整型 (byte, short, int, long) 可以直接用二进制表示.
字面常量数字的下划线. 用下划线连接整数提升其可读性, 自身无含义, 不可
switch 支持 String 类型.
泛型实例化类型自动推断.
- Map<String, List<String>> myMap = new HashMap<String, List<String>>(); // 之前
- Map<String, List<String>> myMap = new HashMap<>(); // 现在
try-with-resources 语句.
- /*
- * 声明在 try 括号中的对象称为资源, 在方法执行完毕后会被自动关闭, 相对与之前必须在 finally 关闭资源, 这一特性大大提高了代码的简洁性.
- * 所有实现 java.lang.AutoCloseable 接口的类都作为资源被自动关闭.
- */
- try (BufferedReader reader=new BufferedReader(new FileReader("d:1.txt"))){
- return reader.readLine();
- }
单个 catch 中捕获多个异常类型 (用 | 分割) 并通过改进的类型检查重新抛出异常
11.4 java8
lambada 表达式(Lambda Expressions)
Lambda 允许把函数作为一个方法的参数(函数作为参数传递进方法中).
方法引用(Method references)
方法引用提供了非常有用的语法, 可以直接引用已有 Java 类或对象 (实例) 的方法或构造器. 与 lambda 联合使用, 可以使语言的构造更紧凑简洁, 减少冗余代码.
默认方法(Default methods)
默认方法允许将新功能添加到库的接口中, 并确保兼容实现老版本接口的旧有代码.
重复注解(Repeating Annotations)
重复注解提供了在同一声明或类型中多次应用相同注解类型的能力.
类型注解(Type Annotation)
在任何地方都能使用注解, 而不是在声明的地方.
类型推断增强
方法参数反射(Method Parameter Reflection)
Stream API
新添加的 Stream API(java.util.stream) 把真正的函数式编程风格引入到 Java 中. Stream API 集成到了 Collections API 里.
HashMap 改进, 在键值哈希冲突时能有更好表现.
Date Time API. 加强对日期和时间的处理.
java.util 包下的改进, 提供了几个实用的工具类.
并行数组排序
标准的 Base64 编解码
支持无符号运算
java.util.concurrent 包下增加了新的类和方法.
java.util.concurrent.ConcurrentHashMap 类添加了新的方法以支持新的 StreamApi 和 lambada 表达式.
java.util.concurrent.atomic 包下新增了类以支持可伸缩可更新的变量.
java.util.concurrent.ForkJoinPool 类新增了方法以支持 common pool.
新增了 java.util.concurrent.locks.StampedLock 类, 为控制读 / 写访问提供了一个基于性能的锁, 且有三种模式可供选择.
HotSpot
删除了 永久代(PermGen)
方法调用的字节码指令支持默认方法
11.5 java9
java 模块系统 (Java Platform Module System).
新的版本号格式.
$MAJOR.$MINOR.$SECURITY.$PATCH
java shell, 交互式命令行控制台.
在 private instance methods 方法上可以使用 @SafeVarargs 注解.
diamond 语法与匿名内部类结合使用.
下划线_不能单独作为变量名使用.
支持私有接口方法(您可以使用 diamond 语法与匿名内部类结合使用).
Javadoc
简化 Doclet API.
支持生成 html5 格式.
加入了搜索框, 使用这个搜索框可以查询程序元素, 标记的单词和文档中的短语.
支持新的模块系统.
JVM
增强了 Garbage-First(G1)并用它替代 Parallel GC 成为默认的垃圾收集器.
统一了 JVM 日志, 为所有组件引入了同一个日志系统.
删除了 JDK 8 中弃用的 GC 组合.(DefNew + CMS,ParNew + SerialOld,Incremental CMS).
properties 文件支持 UTF-8 编码, 之前只支持 ISO-8859-1.
支持 Unicode 8.0, 在 JDK8 中是 Unicode 6.2.
11.6 java10
局部变量类型推断(Local-Variable Type Inference)
- // 之前的代码格式
- URL url = new URL("http://www.oracle.com/");
- URLConnection conn = url.openConnection();
- Reader reader = new BufferedReader(
- new InputStreamReader(conn.getInputStream()))
- //java10 中用 var 来声明变量
- var url = new URL("http://www.oracle.com/");
- var conn = url.openConnection();
- var reader = new BufferedReader(
- new InputStreamReader(conn.getInputStream()));
var 是一个保留类型名称, 而不是关键字. 所以之前使用 var 作为变量, 方法名, 包名的都没问题, 但是如果作为类或接口名, 那么这个类和接口就必须重命名了.
var 的使用场景主要有以下四种:
本地变量初始化.
增强 for 循环中.
传统 for 循环中声明的索引变量.
Try-with-resources 变量.
Optional 类添加了新的方法 orElseThrow. 相比于已经存在的 get 方法, 这个方法更推荐使用.
11.7 java11
支持 Unicode 10.0, 在 jdk10 中是 8.0.
标准化 HTTP Client
编译器线程的延迟分配. 添加了新的命令 - XX:+UseDynamicNumberOfCompilerThreads 动态控制编译器线程的数量.
新的垃圾收集器 - ZGC. 一种可伸缩的低延迟垃圾收集器(实验性).
Epsilon. 一款新的实验性无操作垃圾收集器. Epsilon GC 只负责内存分配, 不实现任何内存回收机制. 这对于性能测试非常有用, 可用于与其他 GC 对比成本和收益.
如果使用选项 - XX:+UseEpsilonGC, 程序很快就因为堆空间不足而退出
使用这个选项的原因 :
提供完全被动的 GC 实现, 具有有限的分配限制和尽可能低的延迟开销, 但代价是内存占用和内存吞吐量.
众所周知, java 实现可广泛选择高度可配置的 GC 实现. 各种可用的收集器最终满足不同的需求, 即使它们的可配置性使它们的功能相交. 有时更容易维护单独的实现, 而不是在现有 GC 实现上堆积另一个配置选项.
主要用途如下 :
1. 性能测试(它可以帮助过滤掉 GC 引起的性能假象)
2. 内存压力测试(例如, 知道测试用例 应该分配不超过 1GB 的内存, 我们可以使用 - Xmx1g -XX:+UseEpsilonGC, 如果程序有问题, 则程序会崩溃)
3. 非常短的 JOB 任务(对象这种任务, 接受 GC 清理堆那都是浪费空间)
Lambda 参数的局部变量语法
java10 中引入的 var 字段得到了增强, 现在可以用在 lambda 表达式的声明中. 如果 lambda 表达式的其中一个形式参数使用了 var, 那所有的参数都必须使用 var
以上绝大部分内容摘抄自 https://github.com/CyC2018/CS-Notes, 仅供学习总结使用, 转载请注明, 谢谢
来源: http://www.jianshu.com/p/ca2ce901df7a