《Java 核心技术 卷一》 -笔记
分类:
文章
•
2022-10-04 11:17:26
-
基础结构
- main 方法应定义为public,在1.4之后才是强制定义的,之前各jvm并未统一。
- 空白符、注释并不出现在编译后程序中,不会造成代码膨胀。
- JDK7+,加上前缀0b(0B)可以写二进制数,还可以为数字字面量加下划线(1_000_000),编译器会自动去除下划线。
- 注释中特别注意\u,\u后跟十六进制,表示特殊字符,(\u00A0为换行,可能导致编译错误)。
-
码点(code point)与代码单元(code unit),码点是unicode中字符的代码值;代码单元用来表示码点(codeUnit 一个为普通字符; 一对则表示辅助字符,原因是字符集已被使用完)。length()方法返回的是代码单元数量,如: “©abc”,使用String的length()结果为5,而codePointCount(0, length())结果为4; 此时使用charAt(1),返回的不是a,而是©的第二个代码单元,慎用char方法;
-
String方法
- IntStream codePoints() [str.codePoints().toArray()]
- int codePointCount(int startIndex, int endIndex)
- Java的变量名为字母与数字构成,但是字母在各个国家语言中定义不同,可使用Character.isJavaIdentifierStart和isJavaIdentifierPart检查是否为可用字母。(最好不使用$,它只用在编译器生成的名字中)。
- 由strictfp关键字,颠覆Java跨平台(依赖于硬件平台)的实际意义。如double w = x * y / q; 若intel处理器,寄存器为80位,则x*y这一中间过程将默认存为80位,除q后再截取为64位。而使用strictfp修饰方法或类。则按照严格浮点计算,中间过程也严格按照64位截取,可能产生浮点溢出。
- Math类为了达到最快性能,完全使用计算机浮点单元的例程,也是依赖于硬件平台。若希望得到完成可预测结果,不计较运行速度,使用StrictMath类。
- +=,++,会产生自动的强制转换,并是自右开始(a+=b+=c : a+=(b+=c))。
- && 比 || 优先级高 a || b && c 等于 a || (b && c)。
- String设计为不可变字符串,是编译器可以让字符串共享,java的设计者认为共享带来的高效率远远胜过修改带来的低效率。
- 如果jvm始终将相同字符串共享,那么==可以检测是否相等。但实际只有字符串常量是共享,通过+等操作的结果并不共享。
- Math.floorMod()解决余数为负数。String.format,String.join, String.compareTo, CharSequence为interface,String/StringBuffer都实现.
- switch: 值(1.char/byte/short/int; 2.enum; 3. 7以后可为String)。fallthrought(break的使用),不能为浮点型。
- array.length;Array.toString();Array.copyOf();Array.sort()采用快速排序;Array.binarySearch()二分查找;Array.fill(type[] a, type v);
- String.format();
- BigInteger的divide提供舍入的模式。
- 不规则数组Array.deepToString();Java并没有多维数组的,只是数组的数组。int[][] a; for(int[] row : a) for (int value: row);
-
对象与类
-
对象三大特征
- 如果不经过方法调用就可以改变对象状态,只能说明封装性遭到了破坏。
- 在Java中,任何对象变量的值都是对存储在另外一个地方的一个对象的引用。
-
Java对象不都存储在堆中,或Java中的逃逸分析和TLAB(Thread Local Allocation Buffer)
- 通过逃逸分析,Hotspot能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。
- 新生代Eden Space中开辟了一小块线程私有的区域,称作TLAB,默认设定为占用Eden Space的1%,在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合被快速GC。
-
LocalDate类
- Java的日期类库有些混乱,已经重新设计了两次。
- Date类的纪元(epoch)是UTC时间1970年1月1日 00:00:00
- 人类的文明与历法的设计紧紧地相连,
- Date类应仅依时间,无关历法,但其中早期的方法如:getDay,getMonth已被标为废弃,日历可使用LocalDate表示。格式化使用DateTimeFormatter(线程安全)。
-
可以对一个字符串数组进行排序,因为String类实现了Comparable<String>
- Arrays.sort([], comparator)
- mutator/accessor method
- 如果在构造器中,定义与实例变量重名的局部变量,这些变量将只能在构造器内部访问。
-
instance.method()
- 隐式参数(implicit),关键字this表示(调用method的实例对象)
- 显式参数(explicit), method方法中显式定义的参数。
-
封装的优点
- 1. 生成只读域,只get
- 2. 执行校验或转换
- 3. 入口唯一
- 不要编写返回引用可变对象的访问器方法,返回可变数据域的拷贝,使用clone是安全之举。
-
Java程序设计语言总是按值调用的
- 对象参数,传递的是对象引用的拷贝,特殊的按值传递。
- 一个方法不能修改一个基本数据类型的参数。
- 方法的签名(signature), 只需要指出方法名以及参数类型。
-
this两个作用
-
初始化数据域
- 在构造器中设置值
- 在声明中赋值
-
初始化块
- 只要构造类的对象,这些块就会执行({}块)
- 首先运行初始化块,然后才运行构造器主体部分
-
执行顺序:
- 1. 所有数据域被初始化为默认值(0,false,null)
- 2. 按照在类声明中出现的次序,依次执行所有域初始化语句和初始化块。
- 3. 执行构造器主体
-
在类第一次加载时,将会进行静态域的初始化。
-
加入静态变量执行顺序:
- 1. 静态域块
- 2. 实例域
- 3. 初始化块
- 4. 构造器
- 只能使用*导入一个 包,而不能使用导入所有包(import java.*)
- 静态导入,不利于代码清晰度
- 如果设置了类路径(classPath)却忘记了包含.目录(当前目录),则javac可以编译,但不能运行
- 优先使用不可变的类,线程安全。
-
默认方法的一个重要用法是接口演化(interface evolution)
- 接口冲突,如果一个超接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型(无论是否是默认参数)相同的方法,必须覆盖这个方法来解决冲突。
-
对象克隆
-
浅拷贝
- 默认的克隆操作 ,并没有克隆对象中引用的其他对象。
-
深拷贝
-
对象对于克隆很偏执,如果一个对象请求克隆,但没有实现Cloneable接口,就会抛出一个CloneNotSupportedException
- 必须重新定义clone为public,才能允许所有方法克隆对象。
- 记号接口,marker interface
-
必须担心子类的克隆
-
不能保证子类的实现者一定会修正clone方法,让它正常工作(子类中新定义了子对象)
- 出于这个原因,在Object类中clone被声明为protected
- clone相当笨拙,标准库中只有不到5%的类实现了clone
- 所有的数组类型都有一个public的clone方法。
-
继承
-
super不是一个对象的引用,只是指示编译器调用超类方法的特殊关键字。
- super调用的构造器,必须是子类构造器的第一条语句。
- 若子类构造器,没有显式调用超类的构造器,则自动调用超类默认构造器,若超类没有默认构造器,则编译报错。
-
方法调用步骤
- 1. 穷举方法
- 2. 重载解析
- 3. 静态绑定、动态绑定(调用方法依赖于隐式参数的实际类型)
- 每次调用方法都要进行搜索,因此JVM预先为每个类创建了一个方法表(所有方法签名和实际调用方法),真正调用方法时,查找这个表就可以了。
- 覆盖一个方法时,子类方法不能低于超类方法的可见性。
- 动态绑定,无需对现有代码进行修改,就可以对程序进行扩展。
-
final
- 如果将一个类声明为final,那其中所有的方法都自动成为final,不包括域。
- JIT编译器,能将能简短,被频繁调用且没有真正被覆盖的方法进行内联处理。
-
进行类型转换的唯一原因:在暂时忽视对象的实际类型之后,使用对象的全部功能。
-
如果试图将父类强制转换为子类,需要进行类型转换,谎报包含内容,则会产生ClassCaseException
- 应进行instanceof检查。尽量少用类型转换和instanceof
-
抽象类
-
proteced
- 表明子类得到信任,可以正确地使用这个方法。(Object.clone())
-
Objects.equals(a, b)
- 在子类中定义equals方法时,首先调用超类的equals。
-
equals
- 自反性,x.equals(x)
- 对称性
- 传递性
- 一致性
- x.equals(null),应该返回false
-
hashCode方法
- 每个对象都有一个默认的散列码,其值为对象的存储地址。
- 字符串的散列码是由内容导出的。
- 如果重新定义equals,就必须重新定义hashCode方法(保证等价的两个对象散列值也相等),以便用户可以将对象插入到散列表中。
- equals和hashCode的定义必须一致。
-
toString
- 使用getClass().getName()获取类名的字符串。
- 随处可见toString方法的主要原因:只要对象与一个字符串通过+连接起来,就会自动调用toString方法。
- Object中定义了toString,用来打印输出对象所属的类名和散列码。
- 强烈建议为每一个自定义的类,增加toStirng方法
-
泛型数组列表(List)
- 使用数组会有不情愿的折中,需要提前确定更大容量的数组。
- JDK7+,可省去右边参数类型: List<A> a = new ArrayList<>();
- ensureCapacity(int );
- trimToSize(),削减容量,可将存储区域的大小调整为当前元素数量所需的空间,回收多余存储空间。
- add(E obj), 永远返回true
- add(int index, E obj)
- 可用@SuppressWarnings("unchecked")标注变量能接受类型转换。
-
装箱与拆箱
- 编译器认可的,而不是JVM;
- 对象包装器类(wrapper)是不可变的
- ArrayList<Integer>效率远远低于int[], 但为了操作的方便性,可牺牲些执行效率。
-
自动装箱autoboxing,调用valueOf()
- list.add(3) -> list.add(Integer.valueOf(3));
-
自动拆箱,调用xxxValue()
- int n = list.get(i) -> int n = list.get(i).intValue();
- boolean, byte, char <= 127, short/int [-128-127]被缓存在对象中。
-
先拆箱再装箱
- 如果条件表达式混合使用Integer和Double, 那么Integer就先拆箱为double,再装箱为Double, true ? integer : double
-
Integer
- parseInt(String s), 返回int
- valueOf(String s), 返回Intger
-
参数可变方法
-
枚举类
- 本质是类实例,每个声明的类型就是一个类,比较时不需使用equals
- Size s = Enum.valueOf(Size.class, "SMALL")
- values();
- ordinal(); 从0开始计数
-
反射
- 分析类能力的程序称为反射
- 使用它的主要人员是工具构造者,而不是应用程序员。
-
Class类
- 程序运行期间,Java运行时系统始终为所有的对象维护一个被称为运行时的类型标识。
-
获得Class类的三种方式
- xx.class
- object.getClass()
- Class.forName(String)
-
一个Class对象表示一个类型,实际是一个泛型类,未必是一个类
-
object.getClass()
- .getName(), 包含包名
-
.newInstance(), 调用默认构造器,创建一个Object实例。
- 调用参数构造器,可以使用Constructor中的newInstance(Object ... initargs)
- Class.forName(String),获得类名对应的Class对象
- getSuperClass()
- JVM为每个类型管理一个Class对象,所有可以使用==实现两个类对象的比较。
- [] getFields()/getMethods()/getConstructors() 返回public型的
- [] getDeclareFields()/getDeclareMethods()/getDeclaredConstructors() 所有的(不包含继承的成员)
-
检查类结构
-
java.lang.reflect
-
Field
- Class getType()
- String getName()
- int getModifiers()
-
Method
- int getModifiers()
- String getName()
-
Constructor
- String getName()
- int getModifiers()
-
Modifier
- static boolean isPublic(int )
- static boolean isPrivate(int )
- static boolean isFinal(int )
- toString(int)
-
使用反射分析对象,受限于Java的访问控制,IllegalAccessException
- 除非有访问权限,否则Java安全机制只允许查看有哪些域,而不允许读取值。
- 可调用AccessibleObject类中static setAccessible(boolean),是Field,Method,Constructor超类。(true表明屏蔽Java的访问检查)
-
Class
- Field getDeclaredField(String )
-
Field
- Object get(Object )
- set(Object, Object)
-
编写泛型数组
-
Object object = Array.newInstance(componentType, newLength);
- int Array.getLength(Object); 返回数组长度
- Class class.getComponentType(); 返回数组对应类型
- boolean class.isArray()
- int[] 与Object可以互相转换,但是Object[]不行
-
Array
-
static xxx getXxx(Object, int index)
- xxx是boolean,byte,char,double,float,int, long,short
- static setXxx(Object, int , Object)
-
调用任意方法
- 使用method对象,可以实现C上的函数指针的所有操作。
-
使用Method中的Obejct invoke(Object , Object ...)
- 第一个参数是隐式参数,其余对象提供显示参数(JDK5-,第一个参数必传,若为static方法,则传null)
-
Method getMethod(String name, Class ... parameterTypes)
- e.g. Employee.class.getMethod("raiseSalary", double.class)
-
缺点
- 必须进行多次的类型转换,将使编译器错过检查代码的机会
- 使用反射获得方法指针要比直接调用方法明显慢一些
- 使用接口或lambda,可使代码执行速度更快,更易于维护。
-
代理
- 利用代理可以在运行时创建一个实现了一组给定接口的新类。
- 在编译时无法确定需要实现哪个接口时才有必要使用。
- 代理类可以在运行时创建全新的类
-
代理类能够实现指定的接口
- 指定接口所需要的全部方法
- Object类中的全部方法(除了clone和getClass),toString, equals...
- 不能定义这些方法新代码,而是要提供一个调用处理器,实现InvocationHandler接口的类对象。
-
创建代理对象
-
只能使用Proxy类的newProxyInstance方法,三个参数
- 类加载器,用null表示使用默认类加载器
- Class对象数组,每个元素都是需要实现的接口,被代理的接口
- 一个调用处理器
- 调用代理对象时,调用会被重定向到调用处理器上。
- 调用处理器中:invoke方法中调用的invoker(target, args)为actual method;可在结尾返回。
-
代理类特性
- 一个代理类只有一个实例域(调用处理器),所有的附件数据都必须存储在调用处理器上。
- 如果使用同一个类加载器和接口数组调用两次newProxyInstance,那么只能够得到同一个类的两个对象。
-
Proxy
- static Class<?> getProxyClass(ClassLoader, Class<?>),返回实现指定接口的代理类
- static boolean isProxyClass(Class<?> ), 是否为代理类
-
内部类
-
1. 内部类
-
为什么需要使用内部类
- 1. 内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据。
- 2. 内部类可以对同一个包中的其他类隐藏起来。
- 3. 当想要定义一个回调函数且不想编写大量代码时,使用匿名内部类比较便捷(可被lambda替代了)
- 内部类既可以访问自身的数据域,也可以访问创建它的外围类对象的数据域。
-
内部类的对象总有一个隐式引用,指向了创建它的外部类对象。
- 编译器修改了所有的内部类的构造器,添加了一个外围类引用参数。
- 只有内部类可以是私有类,而常规类只可以具有包可见性,或公有可见性。
-
内部类特殊语法
- OuterClass..this: 表示外围类引用
- outerObject.new InnerClass(): 内部对象的构造器
-
内部类中声明的所有静态域都必须是final
- 内部类可以允许有静态方法,但只能访问外围类的静态域和方法。
- 内部类是一种编译器现象,与虚拟机无关,编译器将会把内部类翻译成用$分隔外部类名与内部类名的常规类文件。
- 由于内部类拥有访问特权,所以与常规类比较起来功能更加强大。
-
编译器在外围类添加静态方法(access$0)供内部类引用实例域。
- OuterClass.access$0(outerObject)
- 由于隐秘的访问方法需要拥有包可见性,所以攻击代码需要与被攻击类放在同个包下。
- 在虚拟机中不存在私有类,因此编译器将会利用私有构造器生成一个包可见的类。
-
2. 局部内部类
- 在方法中定义局部类
- 局部类不能用public或private进行声明,它的作用域被限定在声明这个局部类的块中。
- 局部类的优势,对外部世界可以完成地隐藏起来。
- 可以访问方法参数,JDK8-必须为final
-
3. 匿名内部类 anonymous inner class
-
创建类的对象
- new SuperType() { inner class}
- 匿名类不能有构造器,将构造器参数传递给超类构造器。
- 可用lambda替代。
-
双括号初始化
- new ArrayList<String>() {{add(""}}
- 外层括号建立了ArrayList的一个匿名子类,内层括号则是一个对象构造块。
- new Object(){} 创建一个匿名对象, getClass().getEnclosingClass(); 获得外围类
-
4. 静态内部类
- 不需要内部类引用外围类时,可使用静态内部类取消引用。
- 只有内部类可以声明为static
- 静态内部类的对象除了没有对生成它的外围类对象的引用特权外,与其他所有内部类完全一样。
- 静态内部类可以有静态域和方法。
-
泛型
- 使用泛型机制编写的程序代码,要比那些杂乱的使用Object变量,然后再进行强制类型转换的代码具有更好的安全性和可读性。
-
Collection中的类型参数
- JDK7+,构造函数可省略类型参数 List<String> list = new ArrayList<>();
-
通配符类型(wildcard type)
- 泛型类可以引入多个不同类型 Pair<T, U>
- 使用E表示集合的元素类型,K和V分别表示表的关键字与值的类型,T(U或S)表示任意类型。
- 泛型类可看作普通类的工厂。精妙
-
泛型方法
-
带有类型参数的简单方法,类型变量放在修饰符的后面,返回类型前面
- public static <T> T getI(T t)
- 调用方法时,Class.<String>getI("i"), 可省略<String>, Class.getI("i");
-
类型变量的限定
- <T extends Comparable>
-
可以有多个限定
- T extends Comparable & Serializable
- 限定中至多有一个类,单继承,如果用类做限定,则必须是列表中的第一个。
- 虚拟机没有泛型类型对象,所有对象都属于普通类
-
类型擦除
- 原始类型的名字就是删去类型参数后的泛型类型名,无限定的变量,直接用Object替换。
-
异常
- 异常处理的任务:将控制权从错误产生的地方转移给能够处理这种情况的错误处理器。
-
异常层次结构
- Error:运行时系统内部错误和资源耗尽错误,除了尽力使程序安全终止之外,再无能为力。
- 如果出现RuntimeException,就一定是你的问题。
- 派生于Error类和RuntimeException类的所有异常称为非受查异常(unchecked),所有其他异常称为受查(checked)异常。
- 受查异常告诉编译器可能发生什么错误,提前准备,体面退出。
- 如果调用一个抛出受查异常的方法(throws),就必须对它进行处理,或者继续传递。
- 将异常直接交给能够胜任的处理器进行处理,要比压制对它的处理更好。
- 子类方法中声明的受查异常(throws)不能比超类方法中的更通用。
-
Throwable
- String getMessage(), 详细描述信息
- Throwable getCause(), 获得原始异常
- printStackTrace(), 获得堆栈情况(或通过Thread.dumpStack()就可不捕获异常,直接获得堆栈轨迹)
- StackTraceElement[] getStackTrace(), 获得构造这个对象时调用堆栈的跟踪信息。
-
捕获异常
- jdk7+,可在catch中捕获多个异常(catch(FileNotFoundException | UnknownHostException e) ), 此时异常变量为final。
- throwable.initCause(e), 原始异常e可设置为新异常的原因,不会丢失原始异常细节。
- jdk7+,编译器会跟踪到异常来自try块内的哪个具体异常,就算方法throws SQLException, catch(Exception e){throw e;}只要e在catch中未改变,方法就是合法的。
-
finally
- 建议解耦try/catch和try/finally语句块,职责单一,提高代码清晰度。
- 若finally中有return语句,该值将覆盖try中return值。
-
finally语句块中,也可能抛出异常(in.close()),
-
若要关闭的资源,实现了AutoCloseable接口,就可使用带资源的try语句(try-with-resources), try () {}
- close抛出的异常将被抑制,可调用getSuppressed()得到被抑制的异常列表。
-
堆栈轨迹(stack trace)
- Thread.getAllStackTraces(),获得所有线程的堆栈轨迹
-
异常机制使用技巧
- 异常处理不能代替简单判断 e.g. 空值的判断
- 不要过分细化异常 e.g.每条语句一个异常
- 受查异常本来就很庞大,不要为逻辑错误抛出这些异常(反射库做法就不正确,需要经常捕获那些早已知道不可能发生的异常??)。
- 不要压制异常。e.g. 要么抛出,要么关闭
- 苛刻比放任要好,返回null(放任)还是抛异常(苛刻)。
- 早抛出,晚捕获(精髓)
-
-
集合
- Java集合类库将接口与实现分离
-
迭代器
- for each是带有迭代器的循环,可以与任何实现Iterable接口的对象一起工作。
- JDK8+ , iterator.forEachRemaining(e -> {});
-
remove会将上次调用的next方法时返回的元素,删除掉
- 调用remove之前没调用next,将会抛出IllegalStateException
-
如果迭代器发现它的集合被另一个迭代器修改了,或被该集合自身方法修改了,就会抛出ConcurrentModificationException
-
Collection
- Java中,所有链表实际上都是双向链接的。
-
Collection
-
List
-
ArrayList
- 默认大小为10
- 扩容, JDK1.6(*1.5+1), JDK6+(*1.5) int newCapacity = oldCapacity + (oldCapacity >> 1);
-
Vector
- 默认大小10
- 扩容(*2),int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);
- 使用方法上synchronized
-
LinkedList
- jdk1.6,双向循环链表; jdk1.7,无循环链表
- ArrayList的增删未必就是比LinkedList要慢,末尾操作.
-
Set
- 底层为HashMap
- HashSet,先判断hash值(哈希值相同的在一个桶里),再进行equals.
- TreeSet,使用二叉树,自定义对象要实现Comparable接口,才能使用排序.
- LinkHashSet
-
Map
-
-
HashMap
- 初始容量:1<<4, 16
- capacity: 当前数组容量,保持在2^n,扩容后,数据*2
-
loadFactor,负载因子,0.75
- 如果内存空间很多而又对时间效率要求很高,可以降低负载因子Load factor的值;相反,如果内存空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值,这个值可以大于1。
- threshold,扩容阀值, = capacity * loadFactor
- modCount字段主要用来记录HashMap内部结构发生变化的次数,主要用于迭代的快速失败。
-
实现
-
JDK8之前,数组中每个元素为一个单向链表
-
数组+链表+红黑树(链表元素超过8后)
-
源码
-
Node[] table; 哈希桶数组
- 使用链地址法解决冲突
-
控制Hash碰撞的概率
-
hashCode()32位 + 高位或运算 + 取模与运算
-
-
取模:return h & (length-1);
- 模运算的消耗还是比较大的,这个方法非常巧妙,当length总是2的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。
- 在HashMap中,哈希桶数组table的长度length大小必须为2的n次方(一定是合数),这是一种非常规的设计,常规的设计是把桶的大小设计为素数。相对来说素数导致冲突的概率要小于合数,HashMap采用这种非常规设计,主要是为了在取模和扩容时做优化.
-
插入成功后,判断实际存在的键值对数量size是否超出了最大容量threshold,如果超过,进行扩容。
-
JDK1.8我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”.
- JDK1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,但是从上图可以看出,JDK1.8不会倒置。
-
线程不安全发生在扩容是,可能导致循环链表Infinite Loop.或者多线程更新同一hash值,可能导致覆盖。
- 在JDK1.7中并发情况下HashMap在扩容时会出现死环现象(原因在于扩容之后链表会倒置呈现);
- 在JDK1.8中扩容之后链表顺序保持不变,避免了死环现象的出现,
-
ConcurrentHashMap
- 由多个Segment(类似HashMap)组成,Segment通过继承ReentrantLock进行加锁.分段锁
- concurrencyLevel: Segment数量,默认16
- TreeMap
- LinkHashMap