Java笔试准备重点拾遗1–基础知识部分

1. 前期绑定与后期绑定

前期绑定指的是编译器在编译时,将调用代码的入口点指向该段代码的绝对地址。
后期绑定/动态绑定指的是编译器在代码执行时,才会决定需要调用的代码。代码地址使用额外的代码段来指定。

和C++不同,Java默认使用此方法来实现多态,而C++需要在基类中显式地声明虚方法(virtual关键字)

2. 基本成员默认值

只有变量作为类的成员使用时,Java才确保给定默认值;某个方法内的局部变量的值仍然可能是任意值。如果没有初始化,程序会报错。

boolean –> false
char –> ‘\u0000′(null)
byte –> (byte)0
short –> (short)0
int –> 0
long –> 0l
float –> 0.0f
double –> 0.0

3. 等价性测试

  • 等于号和不等于测试的是值的相等:对于基础类型,测试的是两个值的相等;对于引用类型,测试的是对象地址的相等(也即是否为同一个对象)

  • equals()测试的是两个对象的相等:如果未重载equals()方法,默认比较的是地址的相等;如果重载了equals()方法,按照重载方法的比较方式处理。对于重用的系统提供的类,它们都实现了自己的equals()方法,如String, Integer, Double等等。

  • 对于小数及其包装类的比较是非常严格的,如果两个小数差一点都会被认作不等。因此建议使用范围相等比较。

4. 移位运算的截尾

  • 对于char、byte、short在移位运算前会被转成int,完成移位运算后截取相应类型的一部分作为结果

  • int、long的移位方式与char、byte、short相同,不过通常不使用类型转换,直接丢弃超出范围的二进制位

  • Java中整数数值的表示法:“有符号的二进制补码”

5. 循环语句

  • 循环语句的终止条件如果存在表达式计算,则在每一次片段中都会被执行一遍。提示不要在终止语句的位置随意使用自增自减操作符,尤其是对for循环而言。

6. Foreach语句

Foreach语句的格式通常为:

for (int i: iList) {
    dosomething(i);
}

Foreach语句可用于任何Iterable对象,比如数组、List、Set等。String类无法直接进行迭代不过可以使用toCharArray()转成char数组。

相较For循环,Foreach循环有以下限制:

  1. Foreach循环无法改变原始值;
  2. Foreach循环无法直接追踪元素下标,这点与Python有很大不同
  3. Foreach循环只能从开始元素起正向递增访问,不能反向访问,也不能随机访问;
  4. Foreach循环不能同时循环两个Iterable对象

7. 使用标号的break和continue语句

Java中不存在goto语句,不过可以通过带有标号的break和continue语句实现一些goto的逻辑。这通常在多层循环的终止中使用。如果我们有一个二层循环且在内层循环中,现在我们希望退出整个循环。在只是用普通break时,我们只能退出内层循环,然后需要一个局部变量帮助我们退出完成循环。使用带有标号的break语句可以一次性退出整个循环体结构。

在此种情况下,标号后面必需紧跟循环语句。

break与continue总结:

  • 一般的continue会跳转至本层循环(通常是内层循环)的顶部,继续执行;
  • 标号continue会跳转至标签所在位置,继续执行紧跟在标签后面的循环;
  • 一般break会跳出当前循环;
  • 带标号的break会跳出标签所指定的循环。
  • 在for循环中,一般continue会自动完成自增自减操作,而带标号的break与continue不会进行自增自减操作,如果有需要需要自行完成。

8. 传参数时的类型匹配

  • 常数:
    整数会优先当做int处理,如果没有匹配的方法可以直接提升至long、float、double(注意,按照此顺序提升);
    小数会当做double处理,其他情况均需要强制类型转换;
    char会优先当做char处理,如果没有匹配的方法会直接升为int、long、float、double(注意,按照此顺序提升);
    如果需要向下窄化匹配,需要手动进行强制类型转换。

  • 变量:
    变量依然按照常量处理的方式,首先看作对应类型的常量,然后依上述方式对变量进行提升,如果提升后有匹配的方法则调用;否则出现错误。

9. 方法重载的判断依据

只有参数列表。两个同名方法的差别可以是参数的个数,参数类型的差异或者是参数类型的顺序(不推荐这么做,这样容易使得程序变得混乱)。返回值并不能用于区分同名方法。

10. 默认构造器

默认构造器是指不含任何参数的构造器。当类中没有任何构造器时,java会为其显式地创造一个;如果存在任何一个程序员定义的构造器,java将不会创造此默认构造器。

11. 使用this显式调用构造器

这种方法可以在一个构造器中调用另一个构造器,以减少代码。注意,这种调用方式仅可以在构造函数中使用
例:

class ThisConstructor {
    String message;
    int code;

    ThisConstructor() {
        this("hello", 5); // 使用this()调用另一个构造函数
    }
    ThisConstructor(String msg, int code) {
        message = msg;
        this.code = code; // 另一个this的常用场景:区分局部变量与类成员
    }
}

12. 终结处理与垃圾回收

finalize()方法

finalize()方法理论上并不需要被执行。它与C++中的析构函数并不一样。官方说明表示finalize()方法在对象被回收的时候完成,但是如果清理工作放在finalize()中可能会引发对象无法被回收从而出现问题。

finalize()通常在Java调用其他语言的代码时用到,它可以释放其他代码所带来的内存分配。另外可以使用finalize()检查对象在释放时是否为期待的值。这将其作为一种逻辑检查的方式。

对象回收的3个要点

  1. 对象可能不被垃圾回收
  2. 垃圾回收不等于析构
  3. 垃圾回收只与内存相关

垃圾回收器的工作方式

  1. 引用计数

此方法存在于java回收机制的讲解中,然而jvm不是这么干的

  1. 停止-复制(stop-and-copy)

此方法将暂停程序的运行,然后将所有存活的对象拷贝到一片新的内存中,然后将之前内存的区域全部释放。

找出存活对象的方式:从堆栈与静态存储区出发,遍历所有的引用,找出存活的对象。

优势:拷贝出的对象在内存中连续存储;解决循环引用造成的垃圾无法释放问题;
劣势:浪费空间;如果垃圾很少时,反复拷贝效率很低

  1. 标记-清扫(mark-and-sweep)

此方法首先找出存活的对象,在找这些对象的过程中,为这些对象进行标记。一轮扫描过后,清扫所有未被标记的对象。

优势:对于短时间出现的垃圾碎片便于回收
劣势:会产生内存空隙

  1. 分块处理

为了提高内存的利用率,java将内存分为若干块。每个块拥有一个代数(generation count)作为其存活的标志。当jvm准备清理内存时,会整理上次垃圾回收后产生的新内存。另外,通过代数可以监视出对象的活跃情况。如果每个块的代数很稳定,说明运行内存很稳定,没有多少块需要释放。

  1. 自适应

JVM结合以上的种种方法,通过自适应的方法决定垃圾回收方式:当对象相对稳定时,jvm会切换至“标记-清扫”模式;当对象碎片较多时,jvm会切换至“停止-复制”模式,以清理散落的内存。

提升速度的其他方式

  • 惰性评估:编译器在需要编译的时候编译代码,并对代码进行优化。这样使得不会被执行的代码不会被编译,常用的代码编译速度越来越快。

Java编程思想第4版:P87-91

13. 成员初始化

指定初始化

可以使用已经计算出的值为成员赋初值。注意,在赋初值的时候右值必需可被计算出来。

指定初始化会在其他初始化方式之前被执行,初始化的顺序按照成员定义的顺序执行。

例:

class A{
    A(int i) {System.out.println("A("+i+")");}
}

class B{
    a1 = new A(1);
    B() {
        System.out.println("B constructor");
        A a = new A(4);
    }
    a2 = new A(2);
    void f() {System.out.println("B::f()");}
    a3 = new A(3);
}

public class Main{
    public static void main(String[] s) {
        B b = new B();
        b.f();
    }
}

输出结果为:

A(1) 
A(2)
A(3)
B constructor
A(4)
B::f()

A(1), A(2), A(3)都是使用指定初始化方式初始化的,它们的执行顺序在构造初始化的前面。这三者的顺序依声明的顺序决定。在指定初始化完成后,进行构造初始化,因此输出B constructor与A(4)。(前面这么多都是主方法中B b = new B();这句引发的。)最后B::f()在调用b.f()时输出。

构造器初始化

构造器初始化在构造器中完成,使用赋值语句。如果一个成员已经使用指定初始化方式赋值了,构造器初始化将会覆盖掉已经初始化的值。

静态类型的初始化

静态类型的变量在类第一次加载的时候被初始化,并且只会在第一次加载的时候被初始化。如果一个类没有被加并且类的成员没有被引用过,这个类就不会被创建,内部的static成员也不会被初始化。静态类型的初始化先于普通成员的初始化。

对象创建过程总结:

  1. 创建类的对象或类的静态方法被访问或类的静态成员被访问时,jvm定位对应的class文件;
  2. 载入class文件,所有静态初始化的方法和参数都会被加载并初始化;
  3. 使用new创建对象的时候,jvm在堆上分配足够的空间;
  4. 按照默认值原则为对象的成员赋“系统”初始值;
  5. 执行指定初始化
  6. 执行构造器初始化

指定构造器的另外写法

使用大括号将指定构造的语句写在一起,置于成员声明的后面。这种写法不会影响初始化的顺序,static成员也可以使用此方法。此种方式在内部类的初始化时有重要作用。

class A {
    int i;
}

class B {
    A a1;
    A a2;
    static int i;
    {
        a1 = new A();
        a2 = new A();
    }
    static {
        i = 1;
    }
}

Java编程思想第4版:P93-98

数组初始化相关

数组可以通过以下方法创建,2、3两种方法还可以进行初始化。这两种方法可以用于参数传递以及返回多个值。数组在创建之后会按照默认值得对应关系为每个元素赋予默认值。

  1. int[] a = new int[3];
  2. int[] a = {1,2,3};
  3. int[] a = new int[]{1,2,3};

可变参数列表

使用数组实现可变参数列表

这种方式下传入的参数是一个数组,由于数组的长度不固定,由此可以实现不定参数的传入机制。参数列表可以指定为通用的Object类型,也可以指定为String、int等特定类型。

class A{
    void f(Object[] obj) {
        for (Object o: obj) {
            System.out.println(o);
        }
    }

    public static void main(String[] s) {
        A a = new A();
        a.f(new Object[]{1,2,3}); // 同一基本数据类型
        a.f(new Object[]{new Integer(1),2.1,"3"}); // 不同类型混合,这是Object的特性
    }
}

使用可变参数列表(SE5特性)

可变参数列表的样式与上述数组相似,不过将[]变为了...,不过传入参数的本质依然是数组,而且是被java包装过的数组。Java会将一系列参数按照类型自动匹配到参数列表中,对于基本类型可能会转变为对应的包装类。如果传入的参数组已经是个数组,java会自动将数组映射至参数列表中。

class A{
    void f(Object... obj) {
        for (Object o: obj) {
            System.out.println(o);
        }
    }
    void g(int p, Object... obj) {
        System.out.println(p);
        for (Object o: obj) {
            System.out.println(o);
        }
    }

    public static void main(String[] s) {
        A a = new A();
        a.f(1,2,3); // 同一基本数据类型,数据罗列
        a.f(new int[]{1, 2, 3}); // 同一基本数据类型,传入已经组成好的数组
        a.f(new Integer(1), 2.1, "3"); // 不同类型混合,这是Object的特性
        a.f(new A(), new A(), new A());
        a.f(); // 空值也可以
        a.g(2, 3, 4); // 2会匹配为p, 3,4会匹配为obj
    }
}

可变参数列表造成的重载问题

对于上面的例子,如果将方法g()改为方法f()是否可以?答案是不可以,因为对于一列int参数,均满足两个f()的定义,jvm无法判断。

还是上面的例子,如果将方法g()改为方法f()并且方法f()中的Object类型改为String类型是否可以?答案是可以,因为对于第一个参数,两个f()接受的类型不同,一个是String,另一个是int。

看以下例子:

class A{
    void f(String... obj) {
        for (String o: obj) {
            System.out.println(o);
        }
    }
    void f(Integer... obj) {
        for (Integer o: obj) {
            System.out.println(o);
        }
    }

    public static void main(String[] s) {
        A a = new A();
        a.f("1", "2");
        a.f(1, 2);
        // a.f()  // ?
    }
}

主方法中最后一行a.f()能够被执行吗?答案是不可以,因为一个空参数列表都可以匹配两个f(),因此会出现错误。不过其他的两个匹配可以完成。

尾随参数

尾随参数是将可变参数列表放在不可变(固定)参数列表的后面,一个方法中最多有一个尾随参数,尾随参数必需放在参数列表的最后。

枚举类型初步(SE5)

创建枚举类型:

public enum ResultCode {
    SUCCESS, RESOURCE_NOT_FOUND, DATABASE_ERROR, INTERNAL_ERROR
}

引用枚举类型的一个值:

ResultCode code = ResultCode.SUCCESS;
System.out.println(code); // 输出SUCCESS

使用ordinal()方法可以返回枚举值按照声明顺序所在的下标,values()可以返回枚举类型的Iterable对象。

枚举类型可以作为标记值,在switch语句中充当标签。

发表评论