Java 基础知识点

本文用于盘点曾经疏忽或者不曾接触的不熟悉的 java 基础知识点.

1.Java权限问题

\begin{align*}\begin{matrix} \ & public & protected & default & private\\\ 同一类中 & OK & OK & OK & OK &\\\ 同一包中 & OK & OK & \textbf{OK} & NO\\\ 子类 & OK & \textbf{OK} & NO & NO\\\ 其他类中 & OK & NO & NO & NO \end{matrix}\end{align*}

加黑内容:子类可以调用父类protected内容.同一个包内可以调用default权限的内容.

2.多态

形如

Map<String, List<String>> map = new HashMap<>();

实现的是 Java 动态绑定的特性. 其实,除了声明为 statics 和 final 的方法, 其他所有方法都是动态绑定的方法. 编译器一直不知道对象的类型, 在运行时能判断对象的类型 ,从而调用恰当的方法. 这一特性使我们在编写 HashMap, 或者 LinkedHashMap 或者 ConcurrentHashMap 时 ,只用按照 Map 提供的诸多方法, 如put(K, V).get(K)等方法填充即可.

这也就意味着如果我们需要在一些地方使用某种 Map 进行某种操作, 我们只需要以 Map 提供的方法为参考标准来设计我们的操作就可以了, 而不用具体到按照 HashMap, 或者 LinkedHashMap 还是 ConcurrentHashMap 的结构来设计, 这就是多态的可拓展性.

public class main {
    public static void main(String[] args){
        Dog Barry = new Samoyed();
        Dog Carlo = new GoldenRetriever();

        Human James = new Human();
        Human Harry = new Son();
        Harry.walkDog(Barry);
        Harry.walkDog(Carlo);

    }
}

class Dog {
    public void bark(){
        System.out.println("Wong, Wong, I am a dog.");
    }
}

class Samoyed extends Dog {
    public void bark(){
        System.out.println("Wong, Wong, I am a Samoyed.");
    }
}

class GoldenRetriever extends Dog {
    public void bark(){
        System.out.println("Wong, Wong, I am a Golden Retriever.");
    }
}

class Human {
    public void walkDog(Dog dog){
        dog.bark();
        System.out.println("Cute doggy!");
    }

    public void walkDog(Samoyed sa){
        sa.bark();
        System.out.println("Cute Samoyed!");
    }

    public void walkDog(GoldenRetriever golden){
        golden.bark();
        System.out.println("Cute Golden Retriever!");
    }
}

class Son extends Human{
    public void walkDog(Dog dog){
        dog.bark();
        System.out.println("Hello doggy!");
    }

    public void walkDog(Samoyed sa){
        sa.bark();
        System.out.println("Hello Samoyed!");
    }

    public void walkDog(GoldenRetriever golden){
        golden.bark();
        System.out.println("Hello Golden Retriever!");
    }
}
/*Print:
Wong, Wong, I am a Samoyed.
Hello doggy!
Wong, Wong, I am a Golden Retriever.
Hello doggy!
*/

这段代码体现的是Java语言的动态单分派(single dispatch)与静态多分派(multiple dispatch).

由于在代码 Dog Barry = new Samoyed(); 中, Dog是static type(静态类型)或者叫apparent type, Samoyed是actual type(实际类型).静态类型的变化仅仅在使用时发生, 变量本身的静态类型不会改变. 并且最终静态类型是在编译期可知的.实际类型在运行期才可确定. 我们使用javap语言查看字节码.可以发现, Harry.walkDog(Barry); 对应的字节码为:

invokevirtual #9 // Method helloworld/wang/rancho/
Human.walkDog:(Lhelloworld/wang/rancho/Dog;)

注意第二个加粗的部分, 说明虚拟机在重载时是通过参数的静态类型而不是实际类型作为判定依据. 编译器会根据参数的静态类型选择使用哪个重载版本. 所以对无论是 Barry 或者是 Carlo, 最后都是 Dog 静态类型.所谓静态多分派.

注意第一个加粗的部分, 说明虚拟机在运行阶段时, 由于先前编译阶段已经决定目标方法的签名, 影响虚拟机选择的只有此方法的接受者实际类型. 虚拟机会在虚方法表(virtural method table, 因为 invokevirtural)索引, 以代替元数据 (metadata) 查找. 如果某个方法在子类中重写, 子类方法表地址将会是子类实现的版本的入口. 总结一下如下表

\begin{align*} \begin{matrix} Overload &Override\\\ 重载 &重写\\\ 静态多分派 &动态单分派\\\ 编译时确定 &运行时确定\\\ \end{matrix} \end{align*}

3.Immutable

Immutable Objects 就是那些一旦被创建, 它们的状态就不能被改变的 Object(s), 每次对他们的改变都是产生了新的 immutable 的对象. 例如String, Integer, Float 等. 不可变对象是线程安全的.在线程之间可以相互共享.不需要利用特殊机制来保证同步问题.因为对象的值无法改变.可以降低并发错误的可能性.因为不需要用一些锁机制等保证内存一致性问题也减少了同步开销.

i. 类添加 final 修饰符.保证类不被继承
ii. 保证所有成员变量必须私有, 并且加上 final 修饰
iii. 不提供改变成员变量的方法, 包括 setter
iv. 通过构造器初始化所有成员, 进行深拷贝 (deep copy)
v. 在 getter 方法中, 不要直接返回对象本身, 而是需要克隆对象, 并返回对象的拷贝 final 关键字用于数据,参数,方法时希望被用于修饰的成员不被改变.

在旧的 Java 版本中,用于方法时会同意编译器将调用转为内嵌调用, 这个特性在新的 JVM 不再成为优势.这体现了高级语言的思想, 应当把效率工作交给编译器和虚拟机. final 修饰的方法能关闭动态绑定.

4. volatile与synchronized 关键字

volatile 并不代表线程安全, 仅仅保证每当变量修改, 所有该变量的缓存会立即生效. 即该变量在各线程中是一致的. 当可修改该变量的作用域唯一时可以使用, volatile 还禁止了指令的重排序优化. volatile 变量读操作的性能消耗很小, 总开销比锁要低. 原子性,可见性,有序性, volatile 实现了后两个.

synchronized 实现了一般意义上的线程安全, Hashtable.Vector, StringBuffer 相比于 Hashmap, ArrayList, StringBuilder 都利用了该关键字. 这也就意味着这三个的响应速度会比较慢, synchronized 是一个重量级(heavyweight)的操作. synchronized 基于jvm. ReentrantLock 基于API.

Java 中, 线程安全是指当多个线程访问一个对象时, 如果不用考虑这些线程在运行时环境下的调度和交替执行, 也不需要进行额外的同步, 或者在调用方进行任何其他的协调操作, 调用这个对象的行为都可以获得正确的结果, 那这个对象是线程安全的. 一般而言.线程方法可以分为 5 类.

i. 不可变. final 关键字实现的 immutable 变量
ii. 绝对线程安全. 不管运行时环境如何, 调用者都不需要任何额外的同步措施, 大多数线程安全的类都不是绝对线程安全.
iii. 相对线程安全. 大部分线程安全的类都是相对线程安全, 如 Vector, HashTable, synchronizedCollection 方法包装的集合等
iv. 线程兼容.在调用端正确使用同步手段来保证对象在并发环境下可以安全使用.大多数线程不安全的类都指的是此情况
v. 线程对立.无论调用断是否采用了同不错是.都无法再多线程环境中并发的代码.例如 Thread.suspend() 与 Thread.resume(). 无论是否进行了同步.并发执行会导致死锁风险.这两个方法已被弃用.

5. 内存区域的划分

牢记下面这张图

i. 左侧为线程共享(方法区与堆, 右侧为线程隔离.
ii. 虚拟机栈中每个方法调用时存在一个栈帧, 用于储存局部变量表, 操作数栈, 动态链接, 方法出口. 局部变量 long double 会占用两个 Slot. 局部变量表的大小在编译期间完成分配. 栈溢出会报 SOF 错误, 允许动态拓展会报 OOM 错误.
iii. 本地方法栈为 Native 方法 (调用非 Java 代码的接口)服务.
iv. 堆是垃圾管理的主要区域, 也称为 GC 堆, 堆可以处于物理上不连续的内存空间.
v. 方法区别名为 Non-Heap, 或者为 Permanent Generation, 区别与 Young Generation 与 Old Generation.

jdk1.6 运行时常量池存在于方法区, 1.7 移去堆中(String.intern() 容易造成PermGen的OOM错误, 1.8 移除永久代, 改为 metaspace, 如下图所示:

将元数据从 PermGen 剥离出来到 Metaspace, 可以提升对元数据的管理同时提升 GC 效率. 在 PermGen 中元数据可能会随着每一次 Full GC 发生而进行移动. HotSpot 虚拟机的每种类型的垃圾回收器都需要特殊处理 PermGen 中的元数据, 分离出来以后可以简化 Full GC 以及对以后的并发隔离类元数据等方面进行优化.后续将 HotSpot 与 JRockit 合二为一做准备. (PermGen 是 HotSpot 的实现特有的,JRockit 并没有 PermGen 一说.) 考虑如下代码:

//Environment : jdk1.8
String str1 = new String("javaa");
String str2 = "javab";
String str3 = new StringBuilder("ja").append("vac").toString();

这段代码在编译时确定了常量池的内容: “javaa”, “javab”, “ja”, “vac” 区别在于: str1 直接在堆上建立了一个 String 对象. str2 在常量池中寻找符合.equal(“javab”) 的对象, 发现后于是将常量池中的拷贝返回. str3 通过新建 StringBuilder 对象, 将 “ja” 与 “vac” 拼接起来, 并在堆上建立了一个 String 对象. 三者可以用intern() 函数测试, String.intern() 函数如果字符串出现在常量池中, 则返回常量池中的拷贝, 否则将拷贝添加到常量池中并返回常量池拷贝:

System.out.println(str1 == str1.intern());//false, str1 is in the heap, not in the pool
System.out.println(str2 == str2.intern());//true, str2 is the copy returned from the pool
System.out.println(str3 == str3.intern());//true, "javac" is added to the pool after intern() is invoked, str3 is the copy copied to the pool

PS. 如果将 str1 修改为

String str1 = new String("javac");

那么

System.out.println(str3 == str3.intern());

将输出 false, 因为在常量池中发现了等值的字符串, intern() 则直接将常量池的 String 返回. 则此时 str3 与之不同.

PSS. 如果将str3改为

String str3 = new StringBuilder("ja").append("va").toString();

也会输出 false, 因为 “java” 常量在运行前就已经存在于常量池中了, 经过不靠谱的测试, 同时还有 “system”, “double”, “char”, “int”, (and other basic arg type) , “false”, “true”. (虽然并没有研究出有什么鸟规律, 也没有查阅到最开始放了哪些进去.)

PSSS. 上述代码测试环境为 jdk1.8, 如果是 jdk1.6 版本, 由于方法区 (PermGen) 的常量池指向的字符串实例也在方法区中, intern() 永远返回的是 PermGen 中的对象, 所以永远不会输出 true. 在jdk1.7 中, 常量池的实例移到了堆中. jdk1.6 与 >jdk1.6 二者的关系如下图所示:

Abstract Class 与 Interface

Abstract 与 Interface 都是为了实现抽象类的定义, 声明抽象类是为了声明方法而不去实现它(等待着继承者去实现). 抽象类与接口都不能被实例化, 但是可以创建一个变量其静态类型是一个抽象类或者接口, 让它指向具体子类或者实现类的一个实例, 例如:

List<Integer> list1 = new ArrayList<>();
AbstractList<Integer> list2 = new ArrayList<>();

除此之外, 两者的实现/子类都必须实现已经声明的抽象方法. 二者区别在于:

i. 一个类只能继承一个超类, 但是一个类可以实现多个接口. (is a / like a)
ii. interface 中每一个方法都是抽象方法, 实现类都必须实现. Abstract Class 可以选择性的实现. 即A bstract Class 包含必须实现(或者子类再次声明为抽象, 此时子类也是抽象类)的抽象方法, 也包含不用实现的非抽象方法, 对于非抽象方法可以直接继承 (Inherit) 也可以覆盖 (Override). iii. interface 是完全抽象的, 仅只能声明 public 方法与常量, 不能声明变量或者其他关键字的方法, 也不能定义方法体 (Not in Java 1.8!
see default method).

Abstract 实际上是 Interface 与 class 的折中.

Interface 的应用场合
i. 类与类之前需要特定的接口进行协调, 而不在乎其如何实现
ii. 作为能够实现特定功能的标识存在, 也可以是什么接口方法都没有的纯粹标识.
iii. 需要将一组类视为单一的类,而调用者只通过接口来与这组类发生联系.
iv. 需要实现特定的多项功能,而这些功能之间可能完全没有任何联系

Abstract class 的应用场合 在既需要统一的接口, 又需要实例变量或缺省的方法的情况下, 就可以使用它. 最常见的有:
i. 定义了一组接口, 但又不想强迫每个实现类都必须实现所有的接口. 可以用 abstract class 定义一组方法体, 甚至可以是空方法体, 然后由子类选择自己所感兴趣的方法来覆盖.
ii. 某些场合下, 只靠纯粹的接口不能满足类与类之间的协调, 还必需类中表示状态的变量来区别不同的关系. Abstract 的中介作用可以很好地满足这一点.
iii. 规范了一组相互协调的方法, 其中一些方法是共同的, 与状态无关的, 可以共享的, 无需子类分别实现; 而另一些方法却需要各个子类根据自己特定的状态来实现特定的功能. 下面是 jdk1.8 中 List.java 与 AbstractList.java 的部分源码, 可以根据上面所说看出比较

/*********File name List.java***********
package java.util;

import java.util.function.UnaryOperator;

public interface List<e> extends Collection</e><e> {
    int size();
    boolean isEmpty();
    boolean contains(Object o);
    Iterator</e><e> iterator();
//...
    boolean add(E e);
//...
    default void replaceAll(UnaryOperator</e><e> operator) {
        Objects.requireNonNull(operator);
        final ListIterator</e><e> li = this.listIterator();
        while (li.hasNext()) {
            li.set(operator.apply(li.next()));
        }
    }
//...
}


/*********File name AbstractList.java***********
package java.util;
public abstract class AbstractList</e><e> extends AbstractCollection</e><e> implements List</e><e> {
    protected AbstractList() {
    }
    public boolean add(E e) {
        add(size(), e);
        return true;
    }
    abstract public E get(int index);
//..
    public int indexOf(Object o) {
        ListIterator</e><e> it = listIterator();
        if (o==null) {
            while (it.hasNext())
                if (it.next()==null)
                    return it.previousIndex();
        } else {
            while (it.hasNext())
                if (o.equals(it.next()))
                    return it.previousIndex();
        }
        return -1;
    }
//...
}
class RandomAccessSubList</e><e> extends SubList</e><e> implements RandomAccess {
    RandomAccessSubList(AbstractList</e><e> list, int fromIndex, int toIndex) {
        super(list, fromIndex, toIndex);
    }

    public List</e><e> subList(int fromIndex, int toIndex) {
        return new RandomAccessSubList<>(this, fromIndex, toIndex);
    }
}
</e>

1 thought on “Java 基础知识点”

Leave a Reply to ggg Cancel reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.