-
内部类
-
使用内部类的3 个原因 Nested Class
- 内部类可以让具有相关逻辑的代码放到同一个类中: 如果某个类只被另外一个类用到,那么这个类可以被定义为另外一个类的内部类.其他类不会关心到这个类的存在.
- 内部类可以增强内聚: (跟 1 类似) 如果有 A-B 两个类, 因为 A类里面某个成员变量需要被 B类访问到, 则 A类的成员变量不能被定义为 private; 如果把 B类定义为 A 类的一个内部类,则 A类的成员变量可以直接被定义为 private . 这些成员变量就不会被其他类访问到.
- 内部类有更好的可读性和可维护性. 被定义为内部类在读代码的时候距离被用到的地方更近.
-
内部类的分类: 按照定义方式分为 4 类. 静态内部类 / 成员内部类 / 局部内部类 / 匿名内部类.
-
其中: 除了静态内部类之外, 其他 3 种内部类都默认持有对外部类对象的引用. (TBC-1 Synthetic Field: When compiled, any inner class will contain a synthetic field which references the top level class. Coincidentally, this is what makes possible to access the enclosing class members from a nested class.)
原因是: (非静态) 内部类对象默认持有对外部类对象的引用, 是因为: 内部类在被 jdk 编译之后生成的 class 的构造方法中, 默认增加了一个指向外部类的变量, 并在构造方法中进行赋值. 因此: 内部类对象默认持有对外部类对象的引用; 但外部类对象默认不持有对内部类对象的引用. 参考这里
-
内部类共同的特点: 编译之后的类名中都会带有 '$'. 局部内部类和匿名内部类还会带有数字.
- 其中, 静态内部类和成员内部类的命名方式为: outer_class_name$inner_class_name.class
- 局部内部类的命名方式为: outer_class_name${1}inner_class_name.class
- 匿名内部类的命名方式为: outer_class_name${1}.class — 因为匿名内部类没有名称所以只有数字
-
静态内部类
定义方式: 定义在外部类中. [private] static class StaticInnerClass { .. }
用法特点:
可以访问外部类的私有静态字段和私有静态方法. 通过
OuterClass.StaticInnerClass()
访问. 不可以直接访问外部类的成员变量
// Outer class 中访问 StaticInnerClass StaticInnerClass staticInnerClass = new StaticInnerClass(); // 其他类中访问 StaticInnerClass, StaticInnerClass 必须不是 private 才能在其他类中访问 OuterClass.StaticInnerClass staticInnerClass = new OuterClass.StaticInnerClass();
-
成员内部类
定义方式: 定义在外部类中. [private] class InstanceInnerClass { .. }
用法特点: 可以直接访问外部类的私有成员变量. 通过
outerClassObject.new InstanceInnerClass()
访问.在成员内部类中不能定义 static 类型的变量, 但可以定义 static final 类型的常量
OuterClass outerClass = new OuterClass(); InstanceInnerClass innerClassObj = new outerClass.new InstanceInnerClass();
- 当在成员内部类中访问外部类的方法(比如 foo 方法)时, 可以直接调用 foo(); 也可以
OuterClass.this.foo()
. 他们之间的区别在于后者明确调用的是外部类的 foo 方法. 如果内部类中没有定义 foo 方法,则两个没有区别; 如果内部类中也有 foo 方法,则会调用内部类的 foo 方法.Mini-Summary:
内部类类型 访问外部类静态常量 访问外部类静态变量 访问外部类成员变量 静态内部类 Yes Yes No (需要 new 外部类的实例然后访问) 成员内部类 Yes Yes Yes -
局部内部类
定义方式: 定义在方法内部. class MethodClass { .. }
用法特点: 跟成员内部类一样, 可以直接访问外部类的成员变量
不能在方法块中定义 interface.
不能在局部内部类中定义static的变量或方法, 但可以定义 static final 的常量
不能在局部内部类中访问方法块中的变量, 但可以访问 final 常量
-
匿名内部类
定义方式: 定义在方法内部. (new Thread() {}).start();
用法特点: 让代码变的简洁. 用法和局部内部类一样除了他没有类名. 如果这个类仅仅会被用到一次, 就可以使用匿名内部类.
常见于定义按钮的回调.
label.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // Handle OnClick event here } });
因为回调这种事件需要处理的频率非常高,如果不用匿名内部类的话我们需要为每个按钮都定义一个类,这样代码冗余太严重.
内部类类型 访问外部类所有类型字段 访问局部常量 访问局部变量 局部内部类 Yes Yes
(从 1.8 开始, 还允许访问 effective final 类型的变量)No 匿名内部类 Yes Yes No 有人说匿名内部类有点 closure 的意思.我目前还没感受到他们之间有什么明确的相似性.
-
-
关于序列化
-
什么是序列化? 为什么要序列化? 如何序列化?
对Java 来说,任何在运行时访问的对象都是存在JVM 的内存当中,当需要把某些对象通过网络发送给其他计算机的时候, 存在内存当中的 Java 对象是不能直接传递给其他计算机的. 这个时候就需要序列化一下.
那什么是序列化呢? 为什么序列化一下就可以传递了呢?
我们知道, 通过网络传输的任何数据都是字节流或者说是数据流,或者干脆简单的理解成二进制. 序列化就是把 Java 对象变成一段二进制数据流的过程.
那么如何序列化呢?
常见的序列化方式有三种, Java 原生的 Serializable 接口; 第三方的Hession 和 JSON. Android 里面还有一种
Parcelable
.序列化的本质是什么?
第一: 提供方把内存中的对象转化成某种约定好格式的数据流(可以是纯文本, 比如 JSON), 我们称之为序列化; 第二: 使用方读取接收到的数据流,按照约定好的格式解析出数据流当中的内容,然后组装成一个对象,就可以在自己的 JVM 运行时使用了.
序列化还有哪些使用场景?
持久化时也可以使用,比如当需要把对象存到磁盘文件或数据库中的时候,也会用到序列化.
-
Serializable
-
Serializable 接口
public class Person implements Serializable { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } }
上面代码中 Person 类实现了 Serializable 接口. 这个 Serializable 接口是一个标记性接口, 代表这个类的对象可以被序列化. 仅此而已,没有抽象方法需要实现.
-
序列化
ObjectOutputStream.writeObject(obj)
这个方法将 obj 这个对象写到对应的输出流中.// personFile 是一个 File 的对象. 具体代码在 demo 项目中 try (FileOutputStream fileOutputStream = new FileOutputStream(personFile); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);) { objectOutputStream.writeObject(person); System.out.println("person file path: " + personFile.getAbsolutePath()); } catch (IOException ioe) { ioe.printStackTrace(); }
-
序列化父类字段了吗?
public class Student extends Person { private int grade; public Student(String name, int age, int grade) { super(name, age); this.grade = grade; } public int getGrade() { return grade; } }
Student 是 Person 的一个子类. 这个情况下当用类似的方法序列化 Student 对象的时候,其父类的成员变量也会被序列化.
// 如果把 Person 类中 implements Serializable 去掉, 在 Student 类中加上 implements Serializable public class Student extends Person implements Serializable { private int grade; public Student(String name, int age, int grade) { super(name, age); this.grade = grade; } public int getGrade() { return grade; } }
这种情况仍然可以序列化成功. 只不过, 父类(Person类)的成员变量不会被序列化.
-
哪些成员变量可以被序列化? 哪些不行?
Non-transient / non-static 的可以被序列化.
如果想要序列化 transient 成员变量, 可以在定义他的类中, 加一对方法:private void writeObject(ObjectOutputStream objectOutputStream) throws Exception`/private void readObject(ObjectInputStream objectInputStream) throws Exception
这两个方法都是 private 的, 既不是 override 的 Object 类,又不是在 Serializable 接口中定义的,那他是怎么在序列化和反序列化过程当中被调用的呢?
是被这两个类(ObjectOutputStream / ObjectInputStream) 反射调用的.
-
我想指定序列化哪些成员变量, 怎么办? Externalizable 接口
当某个类实现
Externalizable
接口就可以完全指定这个类当中哪些成员变量要被序列化.这种方式可以序列化任何类型的变量或常量, 包括
Serializable
接口默认不会序列化的transient
和static
类型的变量.需要实现的方法为:
writeExternal(ObjectOutputStream)
/readExternal(ObjectInputStream)
在这两个方法中分别调用 out.writeXXX 和 in.readXXX 来按照既定的顺序来保存对象的数据和读取数据并设定到对象当中.
@Override public void writeExternal(ObjectOutput out) throws IOException { out.writeUTF(this.getName()); out.writeInt(grade); // private transient int grade; out.writeUTF(SCHOOL); // private static String SCHOOL = "XX_SCHOOL"; } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { String name = in.readUTF(); this.setName(name); int g = in.readInt(); SCHOOL = in.readUTF(); this.grade = g; }
使用方, 与
Serializable
接口一样, 仍然需要用FileOutputStream
/ObjectOutputStream
来做序列化; 以及用FileInputStream
和ObjectInputStream
来做反序列化. -
反序列化
ObjectInputStream.readObject()
FileInputStream fileInputStream = new FileInputStream(xFile); ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); objectInputStream.readObject(); // 此处读取到序列化的 Object
-
都说反序列化不会调用构造方法, 那反序列化时怎么产生的对象? 看这里
反序列化并不是不会调用构造方法, 而是会调用继承链当中第一个没有实现
Serializable
接口的那个类的无参数构造方法.默认情况下, 一个对象的正常序列化实现方式是这个类的整个继承链当中所有的类都可被序列化. 这样才能保证成员变量可以正常的序列化与反序列化. (参见第3点: 序列化父类字段了吗?)
如果在继承链当中有一个类没有实现 Serializable 接口, 那么他必须提供无参数构造方法, 否则在反序列化的时候会出现
no valid constructor
异常.如果整个继承链中都实现了
Serializable
接口的话, 那么默认调用的构造方法是所有类的父类也就是 Object 类, 他的无参数构造方法会被调用, 生成一个对象之后, 先把 static 变量赋值给这个对象(?static 是属于类的为什么会给这个对象?), 再调用 readObject 方法把读到的数据设置到这个对象当中去.除了这个无参数构造方法之外,其他任何构造方法都不会被执行.
实现
Externalizable
接口需要类中有一个 public 无参构造.书上明确写了"使用Java原生序列化需要注意, Java反序列化不会调用类的无参构造方法"。 我的理解是,默认情况(没有父类或父类也实现了 Serializable 接口)确实不会调用类的无参构造, 因为这种情况他调用的是 Object 的无参构造.
-
序列化文件里面存了什么?
在对象序列化后保存的文件里面, 存着类的元信息, 成员变量的类型以及成员变量的值.
反序列化的时候会从序列化文件当中读取到这个类的元信息, 元信息里面记录了这个对象所属的类是实现了 Serializable 接口还是 Externalizable 接口
如果是 implements Serializable 接口, 则执行流程如#7 点所述.
如果是 implements Externalizable 接口, 则要求被反序列化的类必须有无参数构造方法或者没有任何构造方法. (即: 反序列化过程会调用自己的无参构造方法或 Object 的无参构造.). 这种情况父类有无参构造不会被调用, 并且如果只有父类有无参构造自己类没有的话, 反序列化会抛出 no valid constructor 的异常.
-
serialVersionUID 是干什么的?
当 implements Serializable 接口之后, 建议设置 serialVersionUID. 如果不设置, 编译器会根据类的内部实现自动生成一个 serialVersionUID.
serialVersionUID 代表类的版本信息, 不改动类结构, 比如不增加或删除成员变量, 不需要修改此值.
当删除或修改成员变量之后, 如果改动部分可以兼容之前已经序列化的对象, 则不需要改动此值. 在反序列化时, 依然可以成功, 新添加的字段会被设置成默认值.
相反,如果改动部分在反序列化时不兼容之前序列化的对象, 则需要修改此值. 修改之后,当反序列化时会失败, 抛出:
java.io.InvalidClassException: easycoding.ch02.Student; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2
.很明显, serialVersionUID 被保存到了序列化文件中, 那么就产生了下面的这个问题 #10:
-
既然 serialVersionUID 被定义成了 static 那为什么在反序列化的时候可以从序列化文件当中得到? 看这里
serailVersionUID的作用是, 保证在反序列化的时候运行时 JVM 加载的类跟序列化时的类是一样的.
serialVersionUID在序列化的时候是必须要有的, 也就是说是明确要被序列化到结果中的. 程序员不指定他的值的时候会有编译器根据类的内容自动生成.
-
NO. 不要让内部类可序列化! 看这里
非静态内部类默认持有对外部类对象的引用, 因此在序列化内部类的时候会将外部类对象的字段一起序列化.
-
-
Hession (TBC-2 Hession)
-
JSON (TBC-3 JSON/GSON)
- GSON TypeAdapter?
-
Android Parcelable (TBC-4)
-
-
wait & notify & notifyAll 看这里
-
wait 是某线程在得到锁之后, (通常情况下)发现某个条件不满足,然后调用 对象.wait() 方法, 这样就释放了这个锁
synchronized(object) { while(!condition) { // 条件不满足时就 wait object.wait(); } }
-
notify 是某线程在得到锁之后, (经过一些运算)制造出一个条件,使得 wait 线程中的那个条件被满足,然后调用 同一个对象.notify() 方法, 然后等到这个线程释放了锁之后, wait 的那个线程发现条件满足了,就能继续执行他下面的逻辑
synchronized (object) { // 做一些运算使得wait 线程的条件可以满足 object.notify(); // 在唤醒对象之后做一些其他事情. 需要注意在代码块执行完毕之前不会释放锁. wait 部分 wait之后的代码就不能继续执行 }
-
wait / notify 是怎样工作的?
-
monitor: 在 Java 中任何对象都可以当做一个 monitor 来使用. monitor 中有一个单一锁(single lock), 一个访问线程队列(entry queue, 锁竞争) 和一个等待线程队列 (waiting queue, 等待被 notify).
-
如果在某一时刻,一个线程要调用 synchronized 方法或代码块, 但现在已经有一个线程持有该对象锁正在实行,那么调用线程就会进入对象(monitor)的 entry queue 里.
-
当某个持有对象锁 (monitor, 即: synchronized 方法或代码块) 的线程调用这个对象的 wait 方法之后,这个线程就会释放该对象锁, 同时该线程被加入到这个对象(monitor)的waiting queue 等待队列.
-
当另外一个线程调用同一对象的notify 或 notifyAll 方法之后, 其中任意一个或所有处在 waiting queue 中的线程就会被移动到entry queue, 然后开始竞争这个对象锁. 抢对象锁的线程可以继续执行. (没抢到对象锁的线程继续回到 waiting queue? — 此处不确定,我理解是继续到 waiting queue. 或者保留在 entry queue 也说的通,等下一次锁被释放之后,他们就可以去竞争锁)
-
-
为什么 wait 和 notify / notifyAll 必须提前获取一个对象锁? Why wait notify notifyAll must be called inside a synchronized method or block / Why must wait() always be in synchronized block
- 假设 wait 和 notify 不需要提前得到对象锁的话, 举个例子:
-
假设有两个线程,一个读线程, 一个写线程. 不加 synchronize 并行执行,操作一个article 对象
-
读线程调用 article.read() 方法, 此时并没有内容可以读取,所以需要等待,于是他将要调用 article.wait()
-
就在读线程即将调用 article.wait() 方法但尚未调用的时候, CPU可能会调度给了写线程, 写线程会调用 article.write() 然后他会通知读线程,于是写线程调用了 article.notifyAll()
-
CPU 重新调度到读线程, 则现在调用了 article.wait() 方法 (此时他错过了写线程中 article.notifyAll 的唤醒)
-
此时, 如果写线程不再调用 article.notifyAll() 的话, 读线程永远不会被唤醒, 于是就死锁了
- 那么如果按照 Java 的要求,在 wait 和 notify 之前都必须得到对象锁, 这个例子执行过程是怎样的?
- 假设有两个线程,一个读线程, 一个写线程. 他们需要操作一个 article 对象, 在操作之前必须得到 article 的对象锁(monitor)
- 假设读线程先执行,他先获得了 monitor
- 同时写线程也在执行,只不过读线程已经得到了 monitor,那么他只能等monitor被释放之后竞争到这个锁才能执行对 article 的操作
- 读线程此时调用 read 发现没内容,只能 article.wait(). wait 会自动释放锁
- 写线程得到锁, 调用了 article.write() 写入内容并调用 article.notifyAll(). 然后写线程需要释放锁, 一种表现可能是这样, (通常发生在循环中) 写线程发现上次写的内容还没有被处理, 他就不再write 了.而是调用article.wait() , 释放锁
- 读线程得到锁, 发现已经有内容可以读,就调用 article.read() 处理完之后, 就调用 article.notifyAll(), (通常在循环中)发现没有其他可处理的数据, 就调用 article.wait(), 释放锁
- 写线程得到锁,发现上次写的内容已经被处理,则重新执行第 5 步的过程
- 那么如果按照 Java 的要求,在 wait 和 notify 之前都必须得到对象锁, 这个例子执行过程是怎样的?
-
为什么 wait 通常放到 while 循环体内?
假设有三个线程, 线程 A / B / C. A 因为某条件不成立, 所以处于 wait 状态; 线程 B/C 同时在执行, 当 B 执行过程中改变某值达到了唤醒 A 线程的条件,此时在 B线程 notify 或 notifyAll 之后, A被唤醒之前,有可能 C 线程执行的代码已经改变了此值的内容而导致 A的条件不再满足, 那么A线程就需要再次 wait .
As such, when a consumer wakes up, it cannot assume that the state it was waiting for is still valid. It may have been valid in the past, but the state may have been changed after the notify() method was called and before the consumer thread woke up. Waiting threads must provide the option to check the state and to return back to a waiting state in case the notification has already been handled. This is why we always put calls to the wait() method in a loop.
-
notify 和 notifyAll 的区别是什么?
当有多个线程执行同一对象的 wait方法, 都在等待同一对象的唤醒时.
notify 只能唤醒这些线程其中之一, 其他线程无法被唤醒;
notifyAll 可以同时唤醒所有线程, 这些线程都会从 wait 状态继续执行
-
什么情况用 notify 什么情况用 notifyAll
一般来说都用 notifyAll. 除非你知道哪个线程会被唤醒 — 这种情况一般是只有一个线程在wait 状态.
用 notifyAll 还有更加确切的场景. 假设有3个线程都处于 wait 状态, 他们所等待被唤醒的对象是同一个,但是他们 break wait 循环的条件不同, 比如线程 A 期望 x = 1 就不再 wait, 线程 B 期望 x = 2, 线程 C 期望 x = 3; 此时如果我们知道 x = 2, 那么我们期望notify 的线程就是 B, 但是如果我们调用了 notify , 被唤醒的不一定是 B线程,有可能是 A 或 C, 这种情况线程 B 就无法保证被正确唤醒; 如果用 notifyAll 的话, A/B/C 都会被唤醒, 而 A/C 因为 break wait 循环的条件不满足而继续 wait, 线程 B就可以不再 wait.
In general, a thread that uses the wait() method confirms that a condition does not exist (typically by checking a variable) and then calls the wait() method. When another thread establishes the condition (typically by setting the same variable), it calls the notify() method. The wait-and-notify mechanism does not specify what the specific condition/ variable value is. It is on developer’s hand to specify the condition to be checked before calling wait() or notify().
-
-
hashCode & equals
-
object 的 hashCode 是什么? hashCode() 方法返回的值代表什么意义?
Object hashCode 代表一个对象的唯一值, 不同的 object 应该有不同的 hashCode; 而 hashCode 相同的object 我们认为是同一个 object
jdk 中 Object 的 hashCode 方法是一个 native 方法, 默认返回的是一个与内存地址相关的 int 值.
-
为什么 override equals 就必须要 override hashCode?
简单来说, 如果不这样做你可能会得到不符合预期的结果, 也就是 bug.
原因如下:
/** * 这是一个只 override 了equals 但没有 override hashCode 的 class */ public class EqualsObject { private int id; private String name; public EqualsObject(int id, String name) { this.id = id; this.name = name; } @Override public boolean equals(Object o) { if (!(o instanceof EqualsObject)) { return false; } EqualsObject theOther = (EqualsObject) o; if (this.id === theOther.id && Object.equals(this.name, theOther.name)) { return true; } return false; } } /** * 在下面的例子中创建了三个一样的对象 * 我们知道 HashSet 不允许重复对象出现, 因为 EqualsObject override 了 equals 方法 * 这个例子看起来期望是 size 返回 1, 但结果是返回 3 * 原因就是 EqualsObject 没有 override hashCode */ public class Test { public static void main(String[] args) { EqualsObject obj_1 = new EqualsObject(1, "Tom"); EqualsObject obj_2 = new EqualsObject(1, "Tom"); EqualsObject obj_3 = new EqualsObject(1, "Tom"); HashSet hashSet = new HashSet(); hashSet.add(obj_1); hashSet.add(obj_2); hashSet.add(obj_3); System.out.println(hashSet.size()); } } // 上述问题解决方法就是给 EqualsObject 加上 override hashCode 方法. @Override public int hashCode() { return id + name.hashCode(); }
另外可以参考我之前写的为什么 override equals 就必须要 override hashCode, 要表达的意义是一样的.
-
结合 HashMap 看 equals 和 hashCode
- HashMap 的 put 方法是把一对 K,V 保存起来, 以 key 的 hashCode 来计算这对数据应该存到哪个 slot
- 如果两个Key的 hashCode 相等, 那么根据他们的 hashCode 计算出来的 index 就会相等,也就是这两个Key 会落入同一个 slot, 即: 哈希冲突
- 如果两个Key的 hashCode 不同,那么这两个对象产生哈希冲突的概率会低很多, 哈希冲突减少能提高执行效率
- 如果发生了哈希冲突,两个 key 落入同一个 slot 所对应的 bucket, bucket 是一个链表(JDK 8 之后增加了红黑树的实现, 在 bucket 中 node 数量大于等于 7 的时候就会从链表转成红黑树). 此时会比较当前要插入元素的 key以链表形势存在;
- 在插入一个新 node之前会用当前在插入的 key 跟 bucket 中所有的 node 的 key 进行 equals 比较, 如果 equals 返回 true 则用新的 value直接替换原来的 value; 否则会将新元素插入到bucket 链表的头部(JDK 7中是头插法, JDK 8 之后是尾插法)
-
equals / hashCode 相等不相等的几种情况
- equals 相等, 要求 hashCode 必须相等. 因为 equals 相等的话说明直观意义上他们是同一个对象, 则要求在程序意义上也要相等, 即 要求 hashCode 必须相等
- equals 相等, 但 hashCode 不相等; 这种情况会让人感觉有 bug
- equals 不相等, hashCode 相等; 哈希冲突
- equals 不相等, hashCode 也不相等; 这是推荐的做法, 减少哈希冲突
-
-
override - 方法覆写
子类通过集成, 提供了与父类中方法名和参数都相同的方法, 称为override, 方法覆写
- 因为子类可能是延迟加载或者从网络进行加载,所以子类的实现只有在运行时才能确定, 这是动态绑定的一种情况,动态绑定是多态性得以实现的重要因素
- class 加载之后存在于 JVM 内存的元空间(meta-space)
- 动态绑定在 JVM 中的实现是通过方法表
- 如果某个类覆写了父类的方法,则方法表中的方法指向引用会指向子类的实现处
- 方法的覆写需要满足一大两小两相同的限定规则:
- 一大: 方法的访问权限控制符只能变大
- 两小: 返回值类型只能与父类方法相同或是父类方法返回值类型的子类; 子类方法 throws 的 exception只能是与父类方法 throws 的 exception 的类型相同或是父类方法 throws 的 exception 的子类
- 两相同: 方法名相同; 方法参数相同
-
overload - 方法重载
在一个类中可以存在多个方法名一样, 但参数个数或类型不同的方法. 称为方法重载
重载因子:
- 方法名
- 方法参数个数
- 方法参数类型
- 注意: 方法返回值不同不能当做重载
-
方法调用匹配规则
当一个方法有多个重载方法时,JVM 是如何判定应该调用哪个方法的呢?
- 精确匹配
- 精确匹配不成功的情况下,基本数据类型向上转型再次匹配
- 仍不成功的情况下,通过自动拆装箱进行匹配
- 如果还是不匹配,通过可变参数进行匹配
-
静态绑定 / 动态绑定
- 静态绑定: 在 JVM 解析 class 的时候就能确定代码调用的是哪个目标方法,这种绑定称之为"静态绑定".
- invokestatic - 调用静态方法
- invokespecial - 调用 private 方法, 构造方法或者他所实现接口的默认方法
- 如果方法被标记为 final 则可以直接静态绑定
- 动态绑定: 在执行过程中, JVM 运行时期才能确定具体调用哪个方法,这种称为"动态绑定"
- invokevirtual - 调用非 private 方法
- invokeinterface - 调用接口方法
- 这两种调用都需要在运行时根据具体的调用者类型来确定调用那个方法
- 内联缓存: 一种加快动态绑定的优化技术.缓存了动态绑定方法调用过程中调用者的类型以及该类型对应的目标方法, 如果在之后的运行过程中遇到了已经缓存的类型,则可以直接调用为该类型缓存的目标方法.如果没有缓存当前调用者类型的目标方法, 再去从方法表中找.
- 注意: 有很多地方说: 方法重载是静态绑定; 而方法覆写是动态绑定. 这是不准确的, 因为方法在本类被重载, 也可能同时被子类所覆写, 这种情况就是动态绑定.
对于 invokestatic 以及 invokespecial 而言,Java 虚拟机能够直接识别具体的目标方法 而对于 invokevirtual 以及 invokeinterface 而言,在绝大部分情况下,虚拟机需要在执行过程中,根据调用者的动态类型,来确定具体的目标方法。 如果目标方法被标记为 final 则可以直接静态绑定.
-
传参方式? 传值? 还是传引用? Java 里的参数传递都是值复制的传递过程.
public class Test { private static int staticInt = 222; private static String staticString = "old string"; private static StringBuffer staticBuffer = new StringBuffer("StringBuffer-"); public static void main(String[] args) { method(staticInt); println(staticInt); // 222 method(); println(staticInt); // 888 methodString(staticString); println(staticString); // old string methodBuffer(staticBuffer, staticBuffer); println(staticBuffer.toString()); // StringBuffer-append-1,append-2 } private static void method(int staticInt) { staticInt = 777; // 改变的是 staticInt 局部变量值 } private static void method() { staticInt = 888; // 改变的是全局静态变量 } private static void methodString(String staticString) { staticString = "new string"; // 改变的是 staticString 局部变量 } private static void methodBuffer(StringBuffer buffer1, StringBuffer buffer2) { buffer1.append("append-1,"); // buffer1 指向全局 staticBuffer buffer2.append("append-2"); // buffer2 指向全局 staticBuffer buffer1 = new StringBuffer("New Buffer-"); // buffer1 指向新的对象 buffer1.append("append-3"); // 在新对象上操作不影响原来的全局对象 staticBuffer } }
- Java中的方法调用只有传值,没有传引用; 参数传递都是值复制的传递过程; 对于基本数据类型和 string 复制的是他的内容; 对于引用变量, 复制的是他指向对象的首地址.(对于引用变量,当指向其他对象之后,所有的改变都不会影响他原来指向的对象)
- 静态绑定: 在 JVM 解析 class 的时候就能确定代码调用的是哪个目标方法,这种绑定称之为"静态绑定".
-
泛型的几种用法
-
用在类和接口
public class DemoClass<T> { // ... } public interface DemoInterface<T1, T2> { // ... }
-
用在方法
-
用在方法参数
public <T> void doSomething(T t) { // ... }
-
用在方法返回值
public <T> T doSomething(T t) { // .. } public interface DemoInterface<T1, T2> { public T1 doSomething(T2); public T2 doReverse(T1); }
-
方法上的泛型可以跟类上的泛型指代不同类型
public class DemoClass<T> { // 此处方法上的 <T> 是跟类上的 T 可以是不同类型 public <T> T getResult(T t) { // .. } }
-
-
<? extends T>
上界这种泛型表达形式, 在赋值时只能赋值给 T或T 的子类. T 为上界. 且只能读不能写.
写入会提示泛型类型不匹配的错误.
- 为什么往
List<? extends Number>
中add(3)
就会提示不匹配呢?- 我想应该是因为像是 List<? extends T> 这种泛型, 在编译期间根本不知道运行时这个集合里面放的具体是什么类型, 可能是 T 也可能是 T的任意一个子类.
- 所以如果在编写代码的时候往这样一个集合里面添加任何元素都是不能保证类型安全的.
List<? extends Number> 他的意思不是说:遍历所有元素,这些元素都是 Number 类型; 而是说:以 Number 或 Number 的某个子类进行遍历
看这里SOF**You can't add any object to List<? extends T> because you can't guarantee what kind of List it is really pointing to, so you can't guarantee that the object is allowed in that List. The only "guarantee" is that you can only read from it and you'll get a T or subclass of T.**
更详细的解释SOF看这里
假设我们有 Fruit 类, 他的子类是 Apple, Apple 的子类是 AsianApple:
public class UpBoundClass { public static void main(String[] args) { List<Apple> applesList = new ArrayList<>(); List<AsianApple> asianApplesList = new ArrayList<>(); List<Fruit> fruitsList = new ArrayList<>(); List<? extends Apple> upBoundList = applesList; // 这是可以的 upBoundList = asianApplesList; // 这是可以的, AsianApple extends Apple upBoundList = fruitsList; // 这是不可以的. 上界为 Apple upBoundList.add(new Apple()); // 这是不可以的. 因为 <? extends T> 这种泛型不能 add. 可以 clear 或 remove } }
- 为什么往
-
<? super T>
下界这种泛型表达形式, 在赋值时只能赋值给 T 或 T 的父类. 只能写入, 写入时只能传递 T或 T 的子类. 读取的时候会有泛型丢失的问题
List<? super Number>
只能保证插入到这个 List 里面的是 Number 类型或 Number 的子类型, 不能保证取出来的是 Number 类型 — 泛型丢失**You can't read the specific type T (e.g. Number) from List<? super T> because you can't guarantee what kind of List it is really pointing to. The only "guarantee" you have is you are able to add a value of type T (or any subclass of T) without violating the integrity of the list being pointed to.**
更详细的解释SOF看这里public class LowBoundClass { public static void main(String[] args) { List<Apple> applesList = new ArrayList<>(); List<AsianApple> asianApplesList = new ArrayList<>(); List<Fruit> fruitList = new ArrayList<>(); List<? super Apple> lowBoundList = appleList; lowBoundList = assianApplesList; // 这是不对的, 只有 Apple super class 的list 才能赋值 lowBoundList = fruitList; // 这是可以的, Fruit 是 Apple 的 super class } }
-
List / List<Object> / List<?>
-
List 可以赋值给带有任意泛型的List
-
List 可以 add 任意类型的元素, 但由于泛型的不可协变性, 他只能赋值给List 或者不带泛型参数的 List
List<String> stringsList = new ArrayList<>(); List rawList = stringsList; // 这是可以的 List<Object> objectsList = stringsList; // 这是不可以的, 有编译错误 objectsList = rawList; // 这是可以的
-
List<?>
可以赋值给任意类型的带泛型参数的 list. 但不能add 新的元素到List<?>
-
基本数据类型
- boolean - 1 Byte - 没有缓存区间
- byte - 1 Byte - 缓存区间: [-128, 127]
- char - 2 Byte - 缓存区间: [0, 127]
- short - 2 Byte - 缓存区间: [-128, 127]
- int - 4 Byte - 缓存区间: [-128, 127]
- long - 8 Byte - 缓存区间: [-128, 127]
- float - 8 Byte - 没有缓存区间
- double - 8 Byte - 没有缓存区间
- Refvar - 4 Byte - 指向对象的引用
-
包装类
-
缓存区间的作用
Java 默认对基本数据类型的高频区间对应的对象进行了缓存.
以便对这些高频对象进行复用.
当用 Long x = 123l; 进行赋值时, 这个 Long 对象会由 LongCache.cache 产生, 直接调用的是缓存中的对象.
public class Demo { public static void main(String[] args) { Long x1 = 127l; Long x2 = Long.valueOf(127l); println(x1 == x2); // true Long x3 = 128l; Long x4 = Long.valueOf(128l); println(x3 == x4); // false } }
-
对象内存布局
- 任何对象都由2部分构成
- 对象头 - 占 12 个字节
- 对象标记 - 8 个 Byte
- hashCode - 4 Byte
- GC标记 - 1 B
- GC 次数 - 1 B
- 同步锁标记 - 1 B
- 偏向锁标记 - 1 B
- 类元信息 - 4 B - 存储了对象指向他的类元数据的首地址
- 对象标记 - 8 个 Byte
- 实例数据 - 存储类中实例成员变量和所有可见的父类的成员变量
- 如 Integer 的实例成员只有一个
private int value
, 占用 4 个 Byte - 对于 Integer 来讲, 他占用的内存是对象头所占用的 12 个 Byte + 他的实例成员 int value 所占用的 4 个 Byte, 则共是 16 个 Byte
- 如 Integer 的实例成员只有一个
- 再加上必要的内存对齐填充. 任何一个对象在内存中占用的空间大小都是 8 Byte 的整数倍
- 对于 Double 来讲, 他占用的内存空间是 12 个字节的对象头加上 private double value; - 8 个字节. 所以是 12+8=20 Byte.
- 然而, 由于内存对齐的缘故, 一个 Double 占用的内存空间是 24 Byte
- 对象头 - 占 12 个字节
- 任何对象都由2部分构成
-
引用类型
- refvar - 存储一个引用到实际对象的地址, 占用 4 个 Byte
- refobj - 存储实际的对象
-
String / StringBuilder / StringBuffer
- String 是不可变的, 任意一次修改都是先创建一个新的对象然后赋值给原来引用的过程
- StringBuilder , 效率高于 StringBuffer, 但非线程安全, 需要开发人员调用的时候自己保证线程的安全性
- StringBuffer 线程安全
(TBC-1) Synthetic constructs in Java
(TBC-2) Hession
(TBC-3) JSON / GSON, 高级用法
(TBC-4) Android Parcelable
-4-
-
-