- == 和 equals
-==
基本类型域比较,只比较值(short a=2;);用于对象比较时,比较的是两个对象引用是否指向同一个存储区域
int a = 1000; long b = 1000L;
System.out.println(a==b);//true
Integer c = 100,d=100,e=128,f=128;
System.out.println(c==d);//true,值在~128-127,则对象值是从Integer静态内部类的cache[]数组中取
System.out.println(e==f);//false,值范围超过~128-127,则Integer对象会new对象,详见下文33
-s.equals(t)
对象类型比较。多用于比较对象是否相等,s与t可以是字符串字面量,也可以是对象。Java中所有的equals方法都是超级父类Object中equals方法的重载,该方法内部使用 ==
符号来判断对象引用地址是否相等
public boolean equals(Object obj) {
return (this == obj);
}
- 空串和Null串
- 空串:是一个长度(0)和内容(空)的Java对象
- Null串:是一个String类型的变量的值,表示目前没有任何对象与该变量关联。
- String、StringBuffer和StringBuilder(jdk5.0)
- String 是被final修饰不变类,使用
+
拼接字符串时,调用的是StringBuilder的append方法,若若常量池中有字符串,则直接获取,没有则创建- String s1 = new String("xyz");该过程在堆中创建了
一个或两个对象
,若中堆中的常量池无xyz
字面量 ,则会创建xyz
对象,然后new String()
时又会在堆中创建一个对象
- String s1 = new String("xyz");该过程在堆中创建了
- StringBuilder:用于单线程字符串拼接(拼接的效率高于创建对象),超过默认容量(16 characters)自动扩容,长度加16个字符
- String类的对象引用的值是所有线程共享的,是不变的
- StringBuffer与StringBuilder类利用append()方法为字符串添加内容,拼接字符串直接从常量池中取,节省空间,其中前者是线程安全的,利用
<+>
拼接字符串,如果所有字符串在一个单线程中编辑,应该用StringBuilder类,它是虽然多线程不安全,但不需要考虑线程安全时,它的性能更好。
- 对象与对象变量区分
- 对象是一个类的实例
- 对象变量仅仅是引用一个对象,它并不是对象,一个对象变量可以被赋予多个对象地址
- 码点与代码单元
char
数据类型是一个采用UTF-16编码表示Unicode码点的代码单元,Java字符串是由char值序列组成。- 大多数Unicode码点用一个char代码单元就能表示,但辅码需要2个代码单元表示(即2个char)
- System.out.printf(%8.2f,x)
- %后的值将由参数x替换。将以8个字符宽度和小数点后两位小数的精度,来打印x
- 用于printf的转换符(参考P60)
符号 | 类型 | 例子 |
---|---|---|
d | 十进制整数 | 110 |
s | 字符串 | Hello |
c | 字符 | H |
b | 布尔 | True |
h | 散列码 | 42628b2 |
f | 定点符点数 | 1.12 |
- break,continue及return
break
跳出循环最里层循环
continue
结束循环体中的本次循环,继续下一个循环迭代return
结束方法
- OOP(面向对象编程)的特点
-
encapsulation
封装- 封装的关键要求是不能让类中的方法直接访问其他类的实例域(属性/成员变量),程序仅能通过对象的方法与对象数据进行交互(“黑盒”)
-
inheritance
继承。- 继承是子类对超类的扩展
- 子类会继承所有父类的非private属性和方法
- 子类不能重写父类final修饰的方法
- final修饰的类不能被继承
-
多态
在Java中,对象变量是多态的。Override
,重写式多态(运行时多态,动态分派),通过动态绑定实现,是指在执行期间判断对象的实际类型
,根据其实际类型调用相应方法,这种多态是通过函数重写以及向上转型实现。因为
public class DynamicDispatch {
static abstract class Human {
protected abstract void sayHello();
}
static class Man extends Human {
@Override
protected void sayHello() {
System.out.println("man say hello");
}
}
static class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("woman say hello");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();//"man sayHello"
woman.sayHello();//"woman sayHello"
man = new Woman();
man.sayHello();//"woman sayHello"
}
}
- OverLoad,重载式多态(编译时多态,静态分派),重载即方法名相同,参数不同。编译器在重载时是根据参数的`静态类型` 而不是`实际类型` 作为具体方法调用的判断依据。下例中静态类型为Human,human的实际类型是Man,但javac编译器在编译器并不知道,方法的重载只是编译期确定的,例子中可以使用强转调用Man的方法,`sy.sayHello((Man)human)`
public class StaticDispatch {
static abstract class Human {
}
static class Man extends Human {
}
public void sayHello(Human guy) {
System.out.println("hello,guy!");
}
public void sayHello(Man guy) {
System.out.println("hello,gentleman!");
}
public static void main(String[] args) {
Human man = new Man();
StaticDispatch sr = new StaticDispatch();
sr.sayHello(man);//"hello,guy!"
}
}
- 向上转型。父类对象变量的引用指向子类对象,即通过`=` 将子类对象值的引用赋值给父类对象变量
- 向下转型。需要强制转换,并且转换对象的对象类型必须与父类对象变量的引用指向的对象一致。不能把一个父类对象转化为子类对象
- 类型转换的原因:在暂时忽略对象的实际类型后,使用对象的全部功能
扩展
- Java中方法的调用的过程是对应的方法栈桢在虚拟机栈进栈和出栈的过程,其中
每个栈桢都包含一个指向运行时常量池中该栈桢所属的方法的引用
,这些引用一部分会在类加载阶段就被转化为直接引用(非虚方法),这种转化称为静态解析
;另一部分在运行期间才会被转化为直接引用,称为动态连接
- 编译期可知,运行期不可变。Java语言里可以在编译期就确定唯一的方法有,静态方法、私有方法、实例构造器、父类方法4种,再加上被final修饰的方法(尽管它使用invokevirtual指令调用),这些方法被称为
非虚方法
,这些方法在类加载的解析
阶段就会把符号引用转变为明确的直接引用,不必延迟到运行期再执行
- Java中方法的调用的过程是对应的方法栈桢在虚拟机栈进栈和出栈的过程,其中
- 更改器方法与访问器方法
- 更改器方法(mutator method),会改变对象的属性(实例域),例如,属性的set()方法。类似这样的方法会破坏封装性
- 访问器方法(accessor method),只访问对象而不修改对象的方法。例如,属性的get()方法,可以参考源码,String类,LocalDate类中的方法。
final
-
修饰类,类不能被继承,类中的方法自动地会转变为final修饰,这样的类是不可变类
-
修饰属性,初始化必须显示赋值
-
修饰方法,方法不能重写
-
修饰对象变量,该变量不能再引用其他对象,但当前引用的对象的属性可以更改
-
生命:用final修饰类或方法的目的是使其在子类中不会被改变语义
static
- 静态域:归类所有。每个实例对象都有自己一份实例域,并共享静态域(static修饰的属性)
- 静态方法:归类所有,类名+静态方法名调用。不建议用对象调用,静态方法只能访问静态域。
- 情景1:一种方法不需要访问对象的状态,方法所需参数都是显示提供。eg:Math.pow()
- 情景2:一种方法只需要访问类的静态域
- 参数传递
-
Java中采用的是按值传递。
- 对于基本类型(数字/布尔型)成员变量,方法中引用基本类型实例值,方法内部对参数进行更改,不会影响方法外部该参数的值,实际方法调用的参数是实例值的拷贝
- 对于引用类型的参数,方法内部是对该对象变量引用参数的拷贝,原始对象变量与拷贝后的变量共同指向同一个对象实例,所以方法内部对对象实例进行更改后,原始对象变量也改变了,表面上看想是引用传递。实际拷贝的是指向对象实例的原始对象变量值的拷贝。
-
简单说
- 一个方法不能改变基本类型的参数
- 一个方法可以改变一个对象参数的状态
- 一个方法不能让对象参数引用另一个对象
- 类构造建议
- 一定要保证数据私有(保证封装性)
- 一定要对数据初始化(Java会对对象的实例域初始化,但不会对局部变量初始化)
- 不要在类中过多使用基本类型(用其他类代替过多基本类型使用,增强可读性且易于修改)
- 不是所有的域/对象都需要域更改器方法和访问器方法
- 将职责过多的类进行分解
- 优先使用不可变类
- 类名和方法能展示它的职责
- this与super
-
this
当前对象的引用- 情景1:引用隐式参数(即当前对象,而显示参数指的是方法参数),this.methodName()当前对象对方法的调用
- 情景2:调用该类的其他构造器
-
super
只是指示编译器调用超类方法的关键字- 情景1:调用超类的方法
- 情景2:调用超类的构造器
-
注意:调用构造器的语句只能位于另一构造器的第一行
- 方法调用过程
- 编译器查看方法的声明类型和方法名
- 接下来编译器查看调用方法时提供的参数类型
- 如果是priviate/static/final类型方法,构造器将会准确知道调用哪个方法,这种调用也称静态绑定(static binding),与此对应,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定
- 当程序运行,并且采用动态绑定的方法,虚拟机会调用与所引用对象的实际类型最合适的那个类的方法,如果没找到,就到父类查找(显示声明super(),则会直接在超类搜寻)。
- 虚拟机中通过参数类型和返回类型确定一个方法
- 数组的capacity与size
-
capacity
表示数组有保存多少元素的能力 -
size
表示数组实际有多少元素 -
trimToSize(),当确定数组容量不会再变化,调用该方法会将数组空间缩减至当前尺寸。
- Object类
-
Object类是所有类的始祖,如果没有明确指明一个类的超类,那么Object类就会被认为是这个类的超类
- 可以使用Object类对象引用所有类型的对象
- Object类的变量只能作为各种值的通用持有者,要对其中类型进行操作,必须知道对象的原始类型,并进行强制类型转换,才能使用自己定义的方法
- 在Java中,只有基本类型(数字,字符,布尔)不是对象,所有的数组类型(基本数组,对象数组)都扩展了Object类
-
equals()
:比较两个对象是否相等- 如果子类拥有自己相等的概念,则对称性需求将采用getClass()检测
- 如果由超类决定相等的概念,则使用instanceof()检测,这样可以在不同子类对象间进行相等比较
- Object类的本身的equals方法通过判断,对象的hashCode是否相同即地址是否相同,来判断两对象是否相同;如果子类
重写equals方法,建立自己的相等概念,最好重写hashCode方法
,否则子类对象默认还是使用的Object父类的方法
比较的是通过Object的hashcode算法算出的对象的地址
-
hashCode()
- 由字面量内容导出,hashCode码是一样的
String s = "OK";String t = new String("OK"); //true,因为String类 重写 了equals方法及hashCode方法,相同的方法对相同的字面量算出的hashCode值是一致的 //注意,仅仅是hashCode值相等,s 与 t 对象不相等,new 关键字会在内存中开辟新空间 System.out.println(s.hashCode()==t.hashCode());
- 由Object类的默认hashCode方法导出,是对象的存储地址
StringBuilder ss = new StringBuilder(s); StringBuilder st = new StringBuilder(t); //false,StringBuilder使用的是Object的 equals和hashCode方法,返回的是对象的存储地址 //s 与 t 对象变量指向内存中的地址本身就不相同 System.out.println(ss.hashCode()==st.hashCode());
- 相等的对象
hashCode值
一定相等;而hashCode值
相等,对象不一定相等(哈希冲突)
-
toString
- 表示返回对象值的字符串。
- 若是数组调用,则会返回hash值,因为数组继承了Object类;想打印数组可以使用Arrays.toString()
- 数组
-
数组类默认扩展了Object类,没有泛型类时,数组类的get()方法只能返回Oject,因此get()方法调用者必须对返回值进行强制类型转换
-
数组类继承了Object类的toString()方法,正确打印数组是调用静态Arrays.toString()打印
-
数组排序:对象数组类实现comparable
接口,可以调用Arrays.sort()方法 -
Java中数组有一个限制,无法构造泛型类数组,可以用Lambda表达式做到
- 自动装箱/拆箱
-
装箱会造成性能浪费。装箱后的值本质上就是把原始类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值
-
char
boolean
byte
=< 127,short
和int
类型介于-128~127之间会被自动装箱到固定对象中 -
基本类型是只是一个字面量,在一个类中一但被创建只会实例化一次。但是如果是包装类型,与基本类型进行加减运算时,会先自动拆箱进行运算,然后将结果进行装箱(如,Integer.valueOf(sum)),装箱返返回的是一个创建的实例,多次运算会返回多个实例,性能浪费。
-
Java是面向对象的语言,包装类型是一个对象,可以调用方法,而基本类型仅仅是字面量,无法进行其他操作
-
字符串转变为整型
- int x=Integer.parseInt(s)
-
字符串转变为包装类型(Integer)
- Integer x=Integer.valueOf(s)
- 接口与抽象类
-
共同特点,抽取重复代码,设置约束,接口是比抽象类更抽象的抽象类;不同点,抽象类只能单继承,一个类可以实现多个接口
-
接口特点
- 静态方法。在Java SE8 中,允许定义静态方法
- 默认方法。可以为接口提供一个default修饰的默认方法,因为在接口中定义的是完整的方法,所以实现接口的类可以直接调用。接口默认方法与超类冲突/接口冲突解决方法如下
- 超类优先,与接口中同名同参数的默认方法会被忽略
- 接口冲突,必须指定使用的那个接口方法,并且覆盖该方法
- 注意,不能重新定义Object类中的方法为默认方法,类优先规则会使它无效
- 接口中的普通方法默认都是由public abstract修饰(javap 指令反编译查看);若定义了static则是public static 修饰;接口中不允许定义final修饰的方法
-
- 实现类必须实现不含
static
default
修饰的方法
- 实现类必须实现不含
- Comparable
(java.lang)与Comparator(java.util)
-
Comparable
接口, 字典顺序排序
,需要在类内部
重写compareTo()方法- 实现了 Comparable 接口的 List 或则数组可以使用 Collections.sort() 或者 Arrays.sort() 方法进行排序
- 实现了 Comparable 接口的对象才能够直接被用作 SortedMap (SortedSet) 的 key,要不然得在外边指定 Comparator 排序规则。
- 一般自定义类想要进行比较时,除了实现
Comparable
外,还需要重写equals与hashCode
方法
-
Comparator接口,
自定义排序规则
,无法修改实体类时,直接在调用时创建并自定义比较方法
,使用方式如下- 创建一个 Comparator 接口的实现类,并赋值给一个对象
- 在 compare 方法中针对自定义类写排序规则
- 将 Comparator 对象作为参数传递给 排序类的某个方法
- 向排序类中添加 compare 方法中使用的自定义类
22.lambda(P235)
-
格式:参数,箭头(->),表达式
- 编译器可以推导出参数类型时,可以不写参数类型
- 无需指定方法返回类型,它会根据上下文推导。注意,同一个lambda方法在不同地方调用时返回类型必须统一
-
函数式接口:对于只有一个抽象方法的接口,当需要这种接口对象时就可以提供一个lambda表达式。这种接口称函数式接口,例如 comparable 接口
-
方法的引用:对象或类名::方法名
-
Object::instanceMethod
-
例:x->System.out.println(x) 等价于 System.out::println
-
Class::staticMethod
-
例:(x,y)->Math.pow(x,y) 等价于 Math::pow
-
Class::instanceMethod(第一个参数成为方法的调用者)
-
例:(x,y)->x.compareTo(y) 等价于 String::compareTo
-
-
构造器的引用:类名::new
-
lambda表达式引用的变量只能是不会再改变的变量
-
lambda表达式的优点
- 1.延迟执行(deferred execution)
-
- 克隆
-
copy
对对象变量值copy,两个变量指向同一对象 -
Object.clone()
是浅克隆,只能克隆一个对象中引用的是不变类型的对象,如String,LocalDate类,无论自身调用重写clone()方法还是子类调用Object类的clone()方法,都必须实现Cloneable
接口,Object类本身没有实现该接口- x.clone!=x;//true
-
Cloneable
是一个标记接口,内部没有方法,标记接口的唯一作用就是在类型查询时使用instanceOf
-
所有的数组对象都实现了Cloneable接口
-
使用,实现Cloneable接口,重写clone()方法,方法使用
throws
显示声明异常。jdk1.4以后可以自定义返回类型,之前版本只能使用Object返回类型- 浅拷贝。需要拷贝的对象内容是基本类型,或者引用类型的对象是
final
修饰保证其不变性; - 深拷贝。
Cloneable
接口实现。需要拷贝的对象包含引用类型且是可变的,要求引用类型的对象必须实现Cloneable接口,然后再拷贝时需要自己单独
调用clone()方法。Serializable
序列化实现。
- 浅拷贝。需要拷贝的对象内容是基本类型,或者引用类型的对象是
- Proxy
- 代理类是在程序运行过程中创建的,一旦被创建就变成了常规类,与虚拟机中其他类没有区别。
- 所有代理类都扩展于Proxy类。一个代理类只有一个实例域——调用处理器,它是定义在超类Proxy中。为了履行代理对象的职责,所需要的任何附加数据都必须存储在调用处理器中。
- 所有的代理类都重写了Object的equals(),hashCode(),toString()方法,如同所有代理方法一样,这些方法仅仅调用了处理器的invoke
- 调用处理器invocationhandle是实现了InvocationHandle接口的类对象,在这个接口中只有一个方法,无论何时调用代理对象,调用处理器的invoke方法都会被调用,并向其传递Method对象和原始调用参数。
Object invoke(Object proxy,Method method,Object[]args)
创建代理对象,需要使用Proxy类的newProxyInstance方法
newProxyInstance(ClassLoader classloader,Class<?>[]class,InvocationHandle invocationhandle)
- 代理类一定是final和public。
- 内部类
-
特点
- 只有内部类才能定义为私有;内部类可以访问外部类的数据域(成员变量)
- 内部类中声明的所有静态域(成员变量)都必须是final,保证每个外部对象所拥有的内部类实例是唯一的,且内部类不能有static方法,如果设定了静态方法,则只能访问外部类的静态域和静态方法,得不偿失。
- 内部类默认有个指向外部类的引用(OuterClass.this),可以使用如下方法编写内部类的构造器
outObject.new InnerClass(construction parameter)
//创建内部类对象,this代表外部类 this.new InnerClass
-
局部内部类
- 不能用public和private修饰,保证局部内部类对外部类不可见
- 不仅能访问包含局部内部类的外部类,还能访问final修饰的局部变量
-
匿名内部类
- 需要一个对象而不需要对象名
//常归类 Person person=new Person("Tom");
//匿名内部类 Person person=new Person("Cat"){……}
可以在内部类中定义自己的方法,代码更简洁
//常规构造数组对象 ArrayList<String> list=new ArrayList<>(); list.add("Tom"); list.add("Lily"); invite(list);
//匿名内部类构造数组对象 invite(new ArrayList<String>(){{add("Tom");add("Lily");}});
此处外围第一组{}建立了ArrayList的一个匿名子类,第二组内层括号则是一个对象构造块
-
静态内部类(看作类的成员变量)
参考Arrays.AsList(T... a)方法
- 在内部类不需要访问外围对象时使用,用static
- 与常规内部类不同,静态内部类可以有静态域和静态方法
- 声明在接口中的内部类自动成为public和static
- 静态内部类除了没有指向外部的引用外,与其他内部类一样的功能。
- 异常
-
Error
:由于运行时系统内部的错误或资源耗尽错误。(unchecked,非受检查异常) -
Exception
RuntimeException
:程序错误导致(unchecked,非受检查异常)IOException
等其他异常(checked,受检查异常),需要处理
RuntimeException | 非派生于RuntimeException |
---|---|
类型转换异常 | 试图在文件尾部读取数据 |
数组越界 | 试图打开一个不存在的文件 |
访问null指针 | 由给定字符串查找的Class类对象不存在 |
-
处理异常:抛出、捕获、自定义异常类
- 抛出:方法声明时使用throws声明异常类型
- 自定义异常类:派生于Exception或它的子类,定义的类应该包含两个构造器,
默认构造器
和带有详细描述信息的构造器
Throwable();//默认构造器 Throwable(String message);//带有详细描述信息的构造器 String getMessage();//获取Throwable对象的详细信息
-
异常注意事项
- 子类异常不能比父类异常范围还大
- 不能用异常代替简单测试(处理异常很耗时)
- 不要过分地细化异常
- 利用异常结构
- 不要压制异常
- 早抛出,晚捕获
- 断言(assert)
- 断言机制允许在测试期间向代码插入一些检查语句,当代码发布时,这些语句会被自动地移走
- Java语言assert关键字有两种形式
assert 条件;
assert 条件:表达式;
这两种形式都会对条件进行检测,如果结果为false,则会抛出一个AssertionEroor异常,在第二种形式中,表达式将被传入AssertionError构造器中,并转化成一个消息字符串(表达式的唯一目的是产生一个消息字符串,输出控制台,AssertionErrot对象并不会存储它)
- 泛型
-
辅助理解泛型的几个问题
- 泛型 erased(擦除)对类的影响(从泛型成员变量/成员方法,以及继承泛型类的角度分析利弊)
- 泛型使用规范
- 泛型优点
- 泛型生命周期,即是否存在于虚拟机
-
Java类库中,使用变量E表示集合,K和V分别表示表的关键字与值的类型,T(或U或S)可以表示“任意”类型
-
泛型作用在类上:class ClassName <T>
-
泛型用在方法上:
MethodName()
:<T>T -
对泛型变量进行限定:
public static <T extends Comparable&Serializable>T methodName(){}
T是实现了Comparable和Serializable接口的泛型类
- 用
&
对类型变量进行多个限定,而,
用来分隔类型变量。 - 用extends关键字的原因:T和绑定类型(BoundType)既可以是类,也可以是接口,用extends更接近于子类(设计师也没打算再添加关键字)。
- 类型擦除。无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type),原始类型的名字就是删去类型参数后的泛型类型名。例如TimeTest
,原始类型为TimeTest。擦除(erased) 类型变量, 并替换为限定类型 (无限定的变量用 Object),例如
- 用
-
泛型转换事实
- 虚拟机中没有泛型,只有普通的类和方法
- 编译器编译时会擦除泛型变量并用其原始类型代替(第一个限定类型)
- 为保持类型安全性,必要时进行强制类型转换
-
泛型使用注意事项
- 不能用基本类型对泛型变量限定(即泛型只能绑定到引用类型)
- 运行时类型(虚拟机)查询只适用于原始类型,虚拟机中没有泛型,只有普通的类和方法
- 不能实例化类型变量,如
new T(...)
new T[...]
T.class
这样的类型变量是不合法的,Java 8及以后可以用lambda构造器引用来应对这个问题 - 不能构造泛型数组
- 不能在静态域或方法中引用泛型变量
- 不能抛出或捕获泛型类的实例
- 可以消除受检查异常
- 注意擦除后的冲突
-
永远可以将参数化类型转化为转化为一个原始类型
-
继承相关。虽然Manager类继承于Employee类,但两个类作为泛型限定类型时,这样的泛型变量将失去继承关系,是两个无关的类。
-
通配符
?
-
虚拟机中通过参数类型和返回类型确定一个方法
-
通配符子类。Pass<? extends Employee>,
?
此处代表类型是Employee的子类型//编译器不知道应该传入的参数是Employee的哪一个子类型,编译不通过 void setXXX(? extends Employee) //将getXXX()返回值赋给Employee的一个引用,编译通过。因为普通类中可以将子类对象值赋给父类对象变量的一个引用(多态) ? extends Employee getXXX()
-
通配符超类限定。Pass<? super Mannger>,意味着泛型类型变量只能是Manager类或某个子类对象(如Executive),并且此时可以为方法提供参数,但不能使用返回值,此外如果调用getXXX()方法,只能把它的值赋给一个Object
//传入参数是Manager类型,编译通过 void setXXX(? super Manager); //不能保证返回值类型,可能是Manager,也可能是Object类,所以编译器会把返回值赋值给Object,所以不能直接使用它的返回值 ? super Manager getXXX();
-
无限定通配符。Pass<?>
//返回对象是Object类型 ? getXXX(); //方法不能调用(也不能用Object调用),但是可以调用setXXX(null) void setXXX(?);
-
应用,测试Pass是否包含一个null引用,不需要实际类型
public static boolean hasNull(Pass<?> p) { return p.getXXX()==null; }
-
Pass<?>与Pass类的本质不同在于:Pass类可以用任意Object对象调用原始Pass类的setObject方法
-
-
-
综上,带有超类型限定的通配符可以向对象写入,带有子类型限定的通配符可以从泛型对象读取
- 数据域初始化
- 声明时显示指定
- 构造器中指定
- 初始化块中指定
- 调用构造器初始化数据域的顺序
- 所有数据域被初始化为默认值
- 按照在类中声明的次序,依次执行所有域初始化语句和初始化块
- 如果构造器第一行调用了第二个构造器,则执行第二个构造器主体
- 执行这个构造器的主体
-
每个类中显示声明无参构造器的意义
- 子类继承可以构造器中调用父类的构造器,
super()
,注意this()
是调用本类的其它构造器 - 没有显示指定构造器时,编译器会自动的创造一个公有的无参的构造器,即使是抽象类也能被实例化(抽象类例化并没有意义)。避免抽象类实例化或阻止子类调用,父类将无参构造器显h私有即可。
- 子类继承可以构造器中调用父类的构造器,
-
如何判断一个类是否是基本类型,是否是对象实例或子类,是否是数组类型
- Class 对象判断
- 判断是否是基本类型. Xxx.class.isPrimitive():boolean。
- 判断是否是某种基本类型. String.class.isInstance(str):boolean
- 判断是否是对象实例或子类. Xxx.class.isInstance(obj):boolean
- 判断是否是自身类的实例. 自身类.class.isAssignableFrom(自身类或子类.class)
- 判断是否是数组类型. Xxx.class.isArray():boolean
- 获取数组中元素类型. int []ai={1,2,3};ai.getClass.getCompontenType()
- 实例对象判断
- instanceOf
- Class 对象判断
-
序列化(Serializable与Externalizable)
- 什么是序列化?
- 两个客户端TCP/IP通信时(一个send端一个recevied端),形成的报文在传输层是通过
二进制序列
形式在网上传输的。send端的原始数据经过序列化后传输到received端的传输层进行反序列化获取到原始数据,完成通信。 - Java中实现序列化接口后,可以将字节码文件
从内存持久化存储到本地
,将文件从本地加载到内存需要反序列化。 - 序列化指定
serialVersionUID
的好处是,版本控制,相同的ID进行序列化与反序列化保证数据一致性,避免冲突 - 综上、TCP/IP网络上通信、数据需要序列化为二进制格式;将内存中的数据
持久化存储
到本地需要序列化,读取时反序列化。普通的Java类不需要序列化是因为Java程序是运行在内存中的,JVM直接读取字节码二进制文件即可,频繁序列化与反序列化对性能的影响比较大。
- 什么时候不需要使用序列化
- 用户的密码及银行卡号等信息避免在通信时被截取,应该使用
transient
修饰相应字段,使其不被序列化。卡号等信息的生命周期只是在内存中。
- Serializable 与 Externalzable
- 实现 Serializable 接口的类使用
transient
修饰字段时,该字段不会被序列化,将信息持久化到本地后再反序列化后,transient 修饰的字段为null(数字为0) - Externalzable接口继承于 Serializable 接口,实现类重写
readExternal()
与writeExternal()
指定需要序列化的字段,即使字段被transient
修饰,在这两个方法中指明需要序列化,那该字段会被序列化。若未在方法中指明需要序列化的字段,则不会被序列化,即不能持久化字段数据到本地存储。 - 实现Serializable 接口的类的属性都会被序列化,不需要序列化的字段加上
transient
接口即可,若某个类子类实现了 Serializable 接口,父类未实现,则父类的属性不会被序列化;若类实现的是 Externalzable 接口,需要一个一个指定所有字段序列化,很麻烦 - 两种序列化使用场景是,若只需要序列化少量字段,使用
Externalzable
;若只有几个字段不需要序列化,应该使用Serializable
,使用transient
即可。
垃圾回收(内存泄漏)
- 前提:对象,对象变量,对象引用概念 见上文
-
栈(过期引用):栈内部维持着对过期对象(弹出栈的对象)的引用(unintentional object retention,无意识的对象保持)
- 改进:弹栈实际从数组中将一个对象引用放到数组外,但是实际这个在内存中的对象引用还是指向堆中的数据,程序运行久了内存会因为栈中过期对象的增加而不够用(内存泄漏),最终程序终止。每次弹栈后手动将内存中该对象的引用指向
null
则Java的垃圾回收机制会回收内存中这个已弹出栈的对象引用
- 改进:弹栈实际从数组中将一个对象引用放到数组外,但是实际这个在内存中的对象引用还是指向堆中的数据,程序运行久了内存会因为栈中过期对象的增加而不够用(内存泄漏),最终程序终止。每次弹栈后手动将内存中该对象的引用指向
-
缓存(对象引用放到缓存中):随着时间的推移,缓存中对象引用越来越多,需要考虑缓存内容是否有价值及正确设置缓存声生命周期
- 原理:缓存中存放的是个WeakHashMap,缓存的生命周期是由该键的外部引用决定而不是由键的值决定。
- 改进:
方案1
,外部不对缓存对象引用引用,WeakHashMap会自动清理无用的引用;方案2
,缓存用LinkedHashMap表示,当给缓存添加新条目时会清理无用的引用( LinkedHashMap 类利用它的removeEldestEntry);方案3
,清理工作由后台线程(可能是 Timer 或者 ScheduledThreadPoolExecutor )来完成;方案4
,复杂的缓存使用java.lang.ref
来处理
-
监听器和回调
- 如果你实现了一个API,客户端在这个API中注册回调,却没有显式地取消注册,那么除非你采取某些动作,否则它们就会积聚。确保回调立即被当做垃圾回收的最佳方法是只保存它们的弱引用(weak reference),例如,只将它们保存成 WeakHashMap 中的键。
-
排序
- 底层:
Comparable.compareTo(T o)
与Comparator.compare(T o1,T o2)
两个接口都是函数式接口
-
简单排序。实现
Comparable
接口并重写compareTo(T o)方法。按字典顺序排序 -
自定义排序。实现
Comparator
接口,并重写接口中非static
修饰和非default
访问权限的compare(T o1,T o2)
方法 -
为数组排序。
- Arrays.sort(Object[] arrays,Comparator<? super T> c),arrays是待排序数组对象,c是Comparator接口的实例(该类中自定义了排序规则),该接口中的静态方法
comparing()
底层使用的是函数式接口(Comparable),所以提供一个lambda表达式就可以返回一个Comparable接的实例,使用Java8的lambda
表达式优雅排序实例
Arrays.sort(people, Comparator.comparing(Person::getName).thenComparing(Person::getAge).thenComparing(Person::getSalary));
- Arrays.sort(int[] a),数组a可以是所有基本类型的数组对象,
- Arrays.sort(Object[] arrays,Comparator<? super T> c),arrays是待排序数组对象,c是Comparator接口的实例(该类中自定义了排序规则),该接口中的静态方法
-
集合排序。无论是ArrayList还是Map排序底层都调用的是Arrays.sort()方法,集合工具类Collections调用的也是Arrays.sort()方法。
-
Arrays.sort()方法底层对对象数组排序规则是基于
Comparator
接口定义的;对于基本类型的数组,如int[],byte[]等数组,使用的是Java封装的优化的快排,归并
等方法 -
局部变量与成员变量
- 局部变量保存在栈上,属于方法所在的一个线程;成员变量保存在堆中,为所有线程所共享
- Java方法参数是按
值传递
,方法内部调用成员变量(其实是对原来成员变量进行的一个copy,它们指向堆中同一个值),然后对copy的成员变量进行更改,则方法外部对象的成员变量也会更改,因为类的成员变量和方法中copy的成员变量指向的是同一个值。
- Java数值类型缓存(Byte,Short,Integer,Long)
- 关键字:
static class XxxCache{}
static{}
valueOf()
- 未使用 new 创建整型包装类对象时,且值范围在 -128~127之间,类初始化时每个包装类都会有一个缓存静态内部类,通过静态内部类中的静态代码块为内部类的静态 catch[] 数组赋值,cache数组索引范围为 0~256,索引下标对应的值范围为 -128~127,当客户端的值在 -128~127 时,会从缓存中取,超过这个范围,则会new 一个包装类对象
- 关键字: