BAT面试题集锦——Java基础(二)

BAT TMD这些大厂对员工的技术要求都比较高,但是一般面试都会从基础开始,基础面不通过后面的自然不行。而且基础知识都是可以深度挖掘的,遇到那种喜欢从基础知识挖掘的面试官,基础不行肯定面试不通过。所以我们继续为大家整理基础面试的知识点,作为抛砖引玉,希望您能有所收获。

1、问:Java接口存在的意义是什么?
答:
  1. 重要性:在Java语言中, abstract class 和interface 是支持抽象类定义的两种机制。正是由于这两种机制的存在,才赋予了Java强大的面向对象能力。
  2. 简单、规范性:如果一个项目比较庞大,那么就需要一个能理清所有业务的架构师来定义一些主要的接口,这些接口不仅告诉开发人员你需要实现那些业务,而且也将命名规范限制住了(防止一些开发人员随便命名导致别的程序员无法看明白)。
  3. 维护、拓展性:比如你要做一个画板程序,其中里面有一个面板类,主要负责绘画功能,然后你就这样定义了这个类。可是在不久将来,你突然发现这个类满足不了你了,然后你又要重新设计这个类,更糟糕是你可能要放弃这个类,那么其他地方可能有引用他,这样修改起来很麻烦。如果你一开始定义一个接口,把绘制功能放在接口里,然后定义类时实现这个接口,然后你只要用这个接口去引用实现它的类就行了,以后要换的话只不过是引用另一个类而已,这样就达到维护、拓展的方便性。

  4. 安全、严密性:接口是实现软件松耦合的重要手段,它描叙了系统对外的所有服务,而不涉及任何具体的实现细节。这样就比较安全、严密一些(一般软件服务商考虑的比较多)。

其实接口的意义属于编程中的一种规范,具体的意义需要在编程中去感知,没有一个严格的定论。

2、问:泛型中extends和super的区别?
答:

<? extends T>限定参数类型的上界:参数类型必须是T或T的子类型
<? super T> 限定参数类型的下界:参数类型必须是T或T的超类型,类型最高可到Object ,最低是T

泛型的中的通配符挺难理解的,我这里举个例子解释一下:
(1)首先我们定义几个类:

class Fruit {}
class Apple extends Fruit {}
class Orange extends Fruit {}

下面这个例子中,我们创建了一个泛型类Reader,然后在fun1()中当我们尝试:

Fruit f = fruitReader.readExact(apples);

编译器会报错,因为List<Fruit>List<Apple>之间并没有任何的关系。

public class GenericReading {
    static List<Apple> apples = Arrays.asList(new Apple());
    static List<Fruit> fruit = Arrays.asList(new Fruit());
    static class Reader<T> {
        T readExact(List<T> list) {
            return list.get(0);
        }
    }
    static void f1() {
        Reader<Fruit> fruitReader = new Reader<Fruit>();
        // Errors: List<Fruit> cannot be applied to List<Apple>.
        // Fruit f = fruitReader.readExact(apples);
    }
    public static void main(String[] args) {
        fun1();
    }
}

但是按照我们通常的思维习惯,Apple和Fruit之间肯定是存在联系,然而编译器却无法识别,那怎么在泛型代码中解决这个问题呢?我们可以通过使用通配符来解决这个问题:

static class CovariantReader<T> {
    T readCovariant(List<? extends T> list) {
        return list.get(0);
    }
}

static void f2() {
    CovariantReader<Fruit> fruitReader = new CovariantReader<Fruit>();
    Fruit f = fruitReader.readCovariant(fruit);
    Fruit a = fruitReader.readCovariant(apples);
}

public static void main(String[] args) {
    f2();
}

这样就相当与告诉编译器, fruitReaderreadCovariant方法接受的参数只要是满足Fruit的子类就行(包括Fruit自身),这样子类和父类之间的关系也就关联上了。

这个知识点还包含 PECS原则:
上面我们看到了类似<? extends T>的用法,利用它我们可以从list里面get元素,那么我们可不可以往list里面add元素呢?我们来尝试一下:

public class GenericsAndCovariance {
    public static void main(String[] args) {
        // Wildcards allow covariance:
        List<? extends Fruit> flist = new ArrayList<Apple>();
        // Compile Error: can't add any type of object:
        // flist.add(new Apple())
        // flist.add(new Orange())
        // flist.add(new Fruit())
        // flist.add(new Object())
        flist.add(null); // Legal but uninteresting
        // We Know that it returns at least Fruit:
        Fruit f = flist.get(0);
    }
}

答案是否定,Java编译器不允许我们这样做,为什么呢?对于这个问题我们不妨从编译器的角度去考虑。因为List<? extends Fruit> flist它自身可以有多种含义:

List<? extends Fruit> flist = new ArrayList<Fruit>();
List<? extends Fruit> flist = new ArrayList<Apple>();
List<? extends Fruit> flist = new ArrayList<Orange>();
  • 当我们尝试add一个Apple的时候,flist可能指向new ArrayList();
  • 当我们尝试add一个Orange的时候,flist可能指向new ArrayList();
  • 当我们尝试add一个Fruit的时候,这个Fruit可以是任何类型的Fruit,而flist可能只想某种特定类型的Fruit,编译器无法识别所以会报错。

所以对于实现了<? extends T>的集合类只能将它视为Producer向外提供(get)元素,而不能作为Consumer来对外获取(add)元素。

如果我们要add元素应该怎么做呢?可以使用<? super T>

public class GenericWriting {
    static List<Apple> apples = new ArrayList<Apple>();
    static List<Fruit> fruit = new ArrayList<Fruit>();

    static <T> void writeExact(List<T> list, T item) {
        list.add(item);
    }

    static void f1() {
        writeExact(apples, new Apple());
        writeExact(fruit, new Apple());

    static <T> void writeWithWildcard(List<? super T> list, T item) {
        list.add(item)
    }

    static void f2() {
        writeWithWildcard(apples, new Apple());
        writeWithWildcard(fruit, new Apple());
    }

    public static void main(String[] args) {
        f1(); f2();
    }
}

这样我们可以往容器里面添加元素了,但是使用super的坏处是以后不能get容器里面的元素了,原因很简单,我们继续从编译器的角度考虑这个问题,对于List<? super Apple> list,它可以有下面几种含义:

List<? super Apple> list = new ArrayList<Apple>();
List<? super Apple> list = new ArrayList<Fruit>();
List<? super Apple> list = new ArrayList<Object>();

当我们尝试通过list来get一个Apple的时候,可能会get得到一个Fruit,这个Fruit可以是Orange等其他类型的Fruit。
根据上面的例子,我们可以总结出一条规律,”Producer Extends, Consumer Super”:

  • “Producer Extends” – 如果你需要一个只读List,用它来produce T,那么使用? extends T。
  • “Consumer Super” – 如果你需要一个只写List,用它来consume T,那么使用? super T。
  • 如果需要同时读取以及写入,那么我们就不能使用通配符了。

如何阅读过一些Java集合类的源码,可以发现通常我们会将两者结合起来一起用,比如像下面这样:

public class Collections {
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        for (int i=0; i<src.size(); i++)
            dest.set(i, src.get(i));
    }
}

上面这个例子来自于网络,我觉得是目前最能说清楚泛型通配符差异的例子。感兴趣的可以去看看原文:原文链接

3、问:进程和线程的区别
答:
  • (1)进程是资源的分配和调度的一个独立单元,而线程是CPU调度的基本单元
  • (2)同一个进程中可以包括多个线程,并且线程共享整个进程的资源(寄存器、堆栈、上下文),一个进行至少包括一个线程。
  • (3)进程的创建调用fork或者vfork,而线程的创建调用pthread_create,进程结束后它拥有的所有线程都将销毁,而线程的结束不会影响同个进程中的其他线程的结束
  • (4)线程是轻两级的进程,它的创建和销毁所需要的时间比进程小很多,所有操作系统中的执行功能都是创建线程去完成的
  • (5)线程中执行时一般都要进行同步和互斥,因为他们共享同一进程的所有资源
  • (6)线程有自己的私有属性TCB,线程id,寄存器、硬件上下文,而进程也有自己的私有属性进
4、问:final finally finalize区别
答:
  • (1)final修饰符(关键字)。被final修饰的类,就意味着不能再派生出新的子类,不能作为父类而被子类继承。因此一个类不能既被abstract声明,又被final声明。将变量或方法声明为final,可以保证他们在使用的过程中不被修改。被声明为final的变量必须在声明时给出变量的初始值,而在以后的引用中只能读取。被final声明的方法也同样只能使用,不能重载。
  • (2)finally是在异常处理时提供finally块来执行任何清除操作。不管有没有异常被抛出、捕获,finally块都会被执行。try块中的内容是在无异常时执行到结束。catch块中的内容,是在try块内容发生catch所声明的异常时,跳转到catch块中执行。finally块则是无论异常是否发生,都会执行finally块的内容,所以在代码逻辑中有需要无论发生什么都必须执行的代码,就可以放在finally块中。
  • (3)finalize是方法名。java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在object类中定义的,因此所有的类都继承了它。子类覆盖finalize()方法以整理系统资源或者被执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。
5、问:序列化的方式,Serializable和Parcelable的区别
答:

Java序列化的方式有:Java原生以流的方法进行的序列化、Json序列化、FastJson序列化、Protobuff序列化等。
Android可用的序列化方式还有Parcelable。
Serializable和Parcelable的区别:

  • Serializable

位于java.io的路径下,显然是Java通用接口。实际上Serializable就是把Java对象序列化为二进制文件,然后进行传递就非常方便了(这里的传递包括进程之间,程序与网络之间,以及程序与本地存储之间)。使用起来相当简单,比如你自己写一个Person类,只要该类implements了Serializable接口就可以了。好了,这时候你的Person对象就已经实现了序列化,可以通过Intent对象随便跳转了。虽然按照规矩此时应该奉上一段示例代码,但实在是太简单了就算了。

  • Parcelable

通过Serializable接口实现的序列化虽然方便,但却有个“致命”的问题,那就是效率不高,而在这方面有着洁癖的Google表示不能忍,就给我们提供了更高效率的Parcelable,反正大部分开发人员序列化都是为了传递对象而不是保存在本地,使用Parcelable确实更高效。

6、问:父类的静态方法能否被子类重写
答:

静态方法属于静态绑定,在编译阶段已经确定函数名和地址。重写的意思是重新定义父类的虚函数,但是虚函数是动态绑定的,而静态方法是静态绑定的,所以静态函数必然不能是虚函数,也就不存在所说的重写了。你在子类中重新写一个同名的函数,覆盖了父类的同名函数,在使用子类指针进行调用的时候,调用的就是子类的这个静态方法。

我们运营了一个公众号,为了跟大家一起分享学习的知识,感兴趣可以关注:码老板
这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值