Java面试宝典总结

Java基础部分

1.一个“.java”源文件是否可以包括多个类(内部类),有什么限制?

可以有多个类,但只能有一个public类,并且public的类名必须与文件名相一致。

2.&与&&的区别

&与&&都可以用作逻辑与的运算符,表示逻辑与and,当运算符两边的表达式都为true时,整个运算的结果才为true,否则只要一方为false,则结果为false。

&&有短路功能,即如果第一个表达式为false,则不计算第二个表达式。

当&操作符两边的表达式不是boolean类型时,&表示按位与操作,&通常使用0x0f来与一个整数进行&操作,来获取整数的最低4个bit位。

3.在Java中如何跳出当前的多层嵌套循环

让外层的循环条件表达式的结果可以受到里层循环体代码的控制,例如,要在二维数组中查找某个数字。

int[][] arr = {{1,2,3},{4,5,6},{7,8,9}};
boolean found = false;
for(int i = 0; i < arr.length && !found; i++ ){
    for(int j = 0; j < arr[i].length; j++){
        System.out.println("i = " + i + ", j = " + j);
        if(arr[i][j] = 5){
            found = true;
            break;
        }
    }
}

4.short s1 = 1,其中s1 = s1 + 1与 s1 += 1

对于short s1 = 1; s1 = s1 + 1;由于s1 + 1运算时会自动提升表达式的类型,所以结果是int型,在复制给short类型时,编译器将报告需要强制转换类型的错误。对于s1 += 1;由于+=是Java语言规定的运算符,Java编译器会对它进行特殊处理,因此可以正常编译。

5.char类型能不能存储一个中文汉字

char型变量是用来存储Unicode编码的字符串,Unicode编码字符集中包含了汉字,所以char型变量中可以存储汉字,不过如果某个特殊汉字没有被包含在Unicode编码字符集中,那么这个char型变量就不能存储这个特殊汉字。补充说明:Unicode编码占用两个字节,所以,char类型的变量也是占用两个字节。

6.用最有效率的方法算出2乘以8等于多少

2 << 3;位运算,因为将一个数左移n位,就相当于乘以了2的n次方

7.设计一个一百亿的计算器

Java String问题

问题描述

JAVA中String类以形参传递到函数里面,修改后外面引用不能获取到更改后的值

示例代码

public class Temp {
    String str = "good";
    Integer integerParam = 1000;
    Double doubleParam = 2D;
    Long longParam = 1L;
    char[] chars = {'a', 'b', 'c'};

    public void change(Long longParam,
                       Double doubleParam,
                       Integer integerParam,
                       String str,
                       char[] chars) {
        str = "test";
        integerParam = 200000;
        longParam = 10L;
        doubleParam = 3D;
        chars[0] = 'd';
    }

    public static void main(String[] args) {
        Temp temp = new Temp();
        System.out.println("执行方法之前");
        System.out.println("temp.str:" + temp.str);
        System.out.println("temp.integerParam:" + temp.integerParam);
        System.out.println("temp.doubleParam:" + temp.doubleParam);
        System.out.println("temp.longParam:" + temp.longParam);
        System.out.println("temp.chars:"+new String(temp.chars));
        System.out.println();

        temp.change(temp.longParam, temp.doubleParam, temp.integerParam, temp.str, temp.chars);
        System.out.println("执行方法之后");
        System.out.println("temp.str:" + temp.str);
        System.out.println("temp.integerParam:" + temp.integerParam);
        System.out.println("temp.doubleParam:" + temp.doubleParam);
        System.out.println("temp.longParam:" + temp.longParam);
        System.out.println("temp.chars:"+new String(temp.chars));
    }
}

输出结果如下

执行方法之前
temp.str:good
temp.integerParam:1000
temp.doubleParam:2.0
temp.longParam:1
temp.chars:abc

执行方法之后
temp.str:good
temp.integerParam:1000
temp.doubleParam:2.0
temp.longParam:1
temp.chars:dbc

原因

String类的存储是通过final修饰的char[]数组来存储数据的,不可更改。所以当每次外部一个String类型的引用传递到方法内部时候,只是把外部String类型变量的引用传递给了方法参数变量。外部String变量和方法参数变量都是实际char[]数组的引用而已。所以当在方法内部改变这个参数的引用时候,因为char[]数组不可改变,所以每次新建变量都是新建一个新的String实例。很明显外部String类型变量没有指向新的String实例。所以也就不会获取到新的改变。

下面程序例程假定tString指向A内存空间,A内存空间存放了”hello”这个字符串,然后调用modst函数将tString引用赋值给了text引用,注意是引用。确实是传址,我们知道String是不可变的,任何进行更改的操作都会产生新的String实例。所以在方法里面text指向了B空间,B空间存放了”sdf” 字符串,但是这个时候tString还是指向A空间,并没有指向B空间。

public static String modst(String text) {
    return text = "sdf";
}
    String tString = "hello";
    System.out.println(tString);
    //改变text指向
    modst(tString);
    System.out.println(tString);

总结一下三句话

  1. 对象是传引用
  2. 原始类型是传值
  3. String、Integer、Double、Short、Byte等immutable类型因为没有提供自身修改的函数,每次操作都是新生成一个对象,所以要特殊对待。可以认为是传值。

Integer和String一样,保存value的类变量是final属性,无法被修改,只能被重新赋值/生成新的对象。当Integer作为方法参数传递进方法内部时,对其的赋值将会导致原Integer的引用被指向了方法内的栈地址,失去了对原类变量地址的指向。对赋值后的Integer对象做的任何操作,都不会影响原来对象。

Java线程

简介

进程时操作系统分配资源(CPU,内存,IO,磁盘)的单位。

线程是CPU分配时间的单位。

线程状态

创建

在生成线程对象,并且还没有调用该对象的start方法时,此时线程状态为NEW

就绪

调用了线程对象的start方法,并且该线程没有被block,但是此时线程调度程序还没有吧该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或睡眠中回来之后,也处于就绪状态。此时线程状态为RUNABLE。

运行

线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入运行状态,开始执行run方法内的代码。此时线程状态为RUNABLE。

阻塞

线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait,join等方法都可以导致线程阻塞。此时线程的状态有三种:

  • BLOCKED:仅仅会发生在线程期望进入同步代码块或同步方法,并且尚未获得锁的情况下,这有两种可能,一种是线程直接从运行状态抢锁进入同步代码;另一种是线程在执行了wait方法后,被notify/notifyAll(或者wait(long)时间到期)唤醒,然后希望重新抢锁的情况下,可以直接进入blocked状态。处于BLOCKED状态的线程,只有在获得了锁之后,才会脱离阻塞状态。
  • TIMED_WAITING:线程执行了wait(long),sleep(long),wait(long),会触发线程进入TIMED_WAITING状态,在这种状态下与普通的WAITING状态相似,但是当设定时间到了,就会脱离阻塞状态。
  • WAITING:当调用了join()或wait()方法,就会进入WAITING状态,如果是因为调用join()方法进入WAITING状态,则当join的目标线程执行完毕,该线程就会进入到RUNNABLE状态,如果是因为调用wait()进入的WAITING,则需要等待锁对象执行了notify()或notifyAll之后才能脱离阻塞。

注意:无论上面3中哪种阻塞状态,都只能是从运行RUNNING状态转换得来。

死亡

一个线程的run方法执行结束或抛出异常或者调用stop方法并完成对线程的中止之后,该线程死亡。此时线程状态为TERMINATED

Elastic-Job特性

Elastic-Job-Lite

特点

每个周期内使用的Task对象是同一个

Elastic-Job中存在的问题

1.去中心化的限制

  1. 各执行节点调度时间不一致难于控制

  2. 不支持瞬时作业

Elastic-Job-Cloud

1.Mesos能带来什么

  • 应用分发
  • 进程隔离
  • 资源收集

Java类加载和初始化

1.类加载器

每个类编译后产生一个Class对象,存储为.class文件,JVM使用类加载器ClassLoader来加载类的字节码文件(.class),一般的,只会用到一个原生的类加载器,它只加载Java API等可信类,通常只在本地磁盘中加载。如果需要从远程网络或数据库中下载.class文件。需要挂载额外的类加载器。

一般来说,类加载器是按照树形的层次结构组织的。每一个加载器都有一个父类加载器。另外每个类加载器都支持代理模式,既可以自己完成Java类的加载工作,也可以起代理给其他的类加载器。

类加载器的加载顺序有两种,父类有限策略、自己有限策略。父类有限策略是一般的情况(JDK),在这种策略下,类在加载某个Java类之前,会尝试代理给其父类加载器,只有当父类加载器找不到时,才尝试自己加载。自己优先策略正好相反,它会尝试自己去加载,找不到的时候才要父类加载器去加载,这种在Web容器中比较常见(Tomcat)。

2.动态加载

不管是用什么的类加载器,类都是在第一次被使用时,动态的加载到JVM的。这句话有两层含义:

  1. Java程序在运行时并不一定被完全加载,只有当发现该类还没有加载时,才去本地或者远程查找类的.clsss文件并验证和加载。
  2. 当程序创建了第一个对类的静态成员(如类的静态变量、静态方法、构造方法-构造方法也是静态的)的引用时,才会加载该类。Java的这个特性叫做:动态加载

需要区分加载和初始化的区别,加载了一个类的.class文件,不意味着该Class对象被初始化,事实上,一个类的初始化包括三个步骤:

  • 加载(loading):由类加载器执行,查找字节码,并创建Class对象(只是创建)
  • 链接(linking):验证字节码,为静态域分配存储空间(只是分配,并不初始化该存储空间),解析该类创建所需要的对其他类的引用
  • 初始化(initialization):首先执行静态初始化块static{},初始化静态变量,执行静态方法(如构造方法)

链接

Java在加载了类之后,需要进行链接的步骤,链接简单地说,就是讲已经加载的Java二进制代码组合刀JVM运行状态中去。它包括三个步骤:

  1. 验证(verification):验证是保证二进制字节码在结构上的正确性,具体来说,工作包括检测类型正确性,接入属性正确性(public、private),检查final Class没有被继承,检查静态变量的正确性等
  2. 准备(perparation):准备阶段主要是创建静态域,分配空间,给这些域设置默认值。需要注意的是两点:一个是在准备阶段不会执行任何代码,仅仅是设置默认值。二是这些默认值是这样分配的,原生类型全部设为0,如:float 0f,int 0,boolean 0,其他引用类型为NULL
  3. 解析(resolution):解析的过程就是对类中的接口、类、方法、变量的符号引用进行解析并定位,解析成直接引用(符号引用就是编码使用字符串标识某个变量、接口的位置,直接引用就是根据符号引用翻译出来的地址),并保证这些类被正确得找到。解析的过程可能导致其他的类被加载。需要注意的是,根据不同的解析策略,这一步不一定是必须的,有些解析策略在解析时把所有引用解析,这是early relolution,要求所有引用都必须存在。还有一种策略是late relolution,这也是Oracle jdk所采取的策略,即在类只有被引用了,还没有被真正用到时,并不进行解析,只有当真正用到了,才去加载和解析这个类

初始化

static{}是在第一次初始化时执行的,且只执行一次,用下面的代码可以判定出来:

public class Toy {
    private String name;

    public static final int price = 10;

    static {
        System.out.println("initializing");
    }

    public Toy() {
        System.out.println("building");
    }

    public Toy(String name) {
        this.name = name;
    }
}

对上面的类执行下面的代码:

Class c = Class.forName("com.pescod.entity.Toy");

输出信息:

initializing

可以看到,不实例化,只执行forName初始化时,仍然会执行static{}子句,但不执行构造方法,因此输出的只有initializing

根据Java虚拟机规范,所有Java虚拟机实现必须在每个类或接口被Java首次主动使用时才初始化。主要包括下面6中:

  1. 创建类的实例
  2. 访问某个类或者接口的静态变量,或者对静态变量赋值(如果访问的是静态编译时常量(即编译时可以确定值的常量)不会导致类的初始化)
  3. 调用类中的静态方法
  4. 反射(Class.forName(“xxx.xxx”))
  5. 初始化一个类的子类(相当于对父类的主动使用),不过直接通过子类引用父类元素,不会引起子类的初始化(参考实例6)
  6. 被Java虚拟机标明为启动类的类(包含main方法)

示例

示例1

通过上面的讲解,将可以理解下面的程序

public class Toy {

    //静态子句,只在类第一次被加载并初始化时执行一次,而且只执行一次
    static {
        System.out.println("initializing");
    }

    //构造方法,在每次声明新对象时加载
    public Toy() {
        System.out.println("building");
    }
}

对上面的代码段,第一次调用Class.forName(“Toy”),将执行static子句;如果在之后执行new Toy()都只执行构造方法。

示例2

需要注意newInstance方法

//获得类(注意,需要使用含包名的全限定名)
Class cc = Class.forName("Toy");
//相当于new一个对象,但Gum类必须有默认构造方法(无参)
Toy toy=(Toy)cc.newInstance(); 
示例3

类字面常量.class和Class.forName都可以创建对类的引用,但是不同点在于,用.class创建Class对象的引用时,不会自动初始化该Class对象(static子句不会执行)

public class TestToy {
    public static void main(String[] args) {
        Class c = Toy.class; // 不会输出任何值
    }
}

使用Toy.class是在编译期运行的,因此在编译时必须已经有了Toy.Class文件,不然会编译失败,这与Class.forName(“Toy”)不同,后者是运行时动态加载。

但是,如果main方法直接写在Toy类中,那么调用Toy.class,会引起初始化,并输出initilizing,原因不是Toy.class引起的,而是该类中含有启动方法main,该方法会导致Toy类的初始化。

示例4

编译时常量,回到完整的Toy类,如果直接输出:System.out.println(Toy.price),会发现static子句和构造方法都没有被执行,这是因为在Toy中,常量price被static final限定,这样的常量叫做编译时常量,对于这种常量,不需要初始化就可以读取。

编译时常量必须满足三个条件:static、final、常量

下面几种都不是编译时常量,对他们的引用,都不会引起类的初始化:

static int a;
final int b;
static final int c = ClassInitialization.rand.nextInt(100);
static final int d;
static {
    d = 5;
}
示例5

static块的本质,注意下面的代码

class StaticBlock {
    static final int c = 3;
    static final int d;
    static int e = 5;
    static {
        d = 5;
        e = 10;
        System.out.println("Initializing");
    }

    StaticBlock() {
        System.out.println("Building");
    }
}

public class StaticBlockTest {
    public static void main(String[] args) {
        System.out.println(StaticBlock.c);
        System.out.println(StaticBlock.d);
        System.out.println(StaticBlock.e);
    }
}

执行一下,结果为:

3
Initializing
5
10

原因是这样的:输出c时,由于c是编译时常量,不会引起类初始化,因此直接输出,输出d时,d不是编译时常量,所以会引起初始化操作,即static块的执行,于是d被赋值为5,e被赋值为10,然后输出Initializing,之后输出d=5,e=10

但e为什么是10呢?原来,JDK会自动为e的初始化创建一个static块,所以上面的代码等价于:

class StaticBlock {
    static final int d;
    static int e;
    static {
       e=5; 
    }

    static {
        d = 5;
        e = 10;
        System.out.println("Initializing");
    }

    StaticBlock() {
        System.out.println("Building");
    }
} 

可见,按顺序执行,e先被初始化为5,再被初始化为10,于是输出了10

类似的,容易想到下面的代码:

class StaticBlock {
    static {
        d = 5;
        e = 10;
        System.out.println("Initializing");
    }

    static final int d;

    static int e = 5;

    StaticBlock() {
        System.out.println("Building");
    }
}

在这段代码中,将e的声明放到了static块后面,于是,e先被初始化为10,在被初始化为5,所以这段代码中e会输出为5

示例6

当访问一个Java类或接口的静态域时,只有真正声明这个域的类或接口才会被初始化

class B {
    static int value = 100;
    static {
        System.out.println("Class B is initialized");// 输出
    }
}

class A extends B {
    static {
        System.out.println("Class A is initialized"); // 不输出
    }
}

public class SuperClassTest {
    public static void main(String[] args) {
        System.out.println(A.value);// 输出100
    }
}

输出:

Class B is initialized
100

在该例子中,虽然通过A来引用了value,但value是在父类B中声明的,所以只会初始化B,而不会引起A的初始化。