java 超详细面经整理(持续更新)2019.12.19

2019-12-19 05:50:27来源:博客园 阅读 ()

新老客户大回馈,云服务器低至5折

java 超详细面经整理(持续更新)2019.12.19

目录

  • Java SE
    • 请你解释HashMap中为什么重写equals还要重写hashcode?
    • 请你介绍一下map的分类和常见的情况
    • 请你讲讲Java里面的final关键字是怎么用的?
    • 请你谈谈关于Synchronized和lock
    • 请你介绍一下volatile?
    • 请你介绍一下Syncronized锁,如果用这个关键字修饰一个静态方法,锁住了什么?如果修饰成员方法,锁住了什么?
    • 请解释Java中的概念,什么是构造函数?什么是构造函数重载?什么是复制构造函数?
    • 请说明Java中的方法覆盖(重写)(Overriding)和方法重载(Overloading)是什么意思?
    • 忽略:请说明Query接口(Hibernate)的list方法和iterate方法有什么区别?
    • 请你谈一下面向对象的"六原则一法则"。
    • 请说明如何通过反射获取和设置对象私有字段的值?
    • 请你谈谈如何通过反射创建对象?
    • 内部类的详解
      • 请说明内部类可以引用他包含类的成员吗,如果可以,有没有什么限制吗?
      • 内部类的修饰符?
      • 为什么成员内部类可以无条件访问外部类的成员?
      • 为什么局部内部类和匿名内部类只能访问局部final变量?(忽略)
      • 静态内部类有特殊的地方吗?
      • 为什么在Java中需要内部类?
      • 测试
    • 请说明JAVA语言如何进行异常处理,关键字:throws,throw,try,catch,finally分别代表什么意义?在try块中可以抛出异常吗?
    • 请说明Java的接口和C++的虚类的相同和不同处。(即java的抽象类和接口的相同和不同之处)
    • 抽象类和接口中方法方法修饰符问题
    • 请说明一下final, finally, finalize的区别。
    • 请说明面向对象的特征有哪些方面
    • 请说明Comparable和Comparator接口的作用以及它们的区别。
    • 是否可以在static环境中访问非static变量?
    • 泛型
      • 请你讲讲什么是泛型?
      • 请解释一下extends 和super 泛型限定符
      • 泛型通配符 ? 与 T 的区别
    • 请你解释一下类加载机制,类加载器的作用,双亲委派模型,好处是什么?
    • 请你谈谈StringBuffer和StringBuilder有什么区别,底层实现上呢?
    • 请说明String是否能继承?
    • 请解释一下String为什么不可变?以及String不可变的好处有哪些?
    • 请说明”static”关键字是什么意思?Java中是否可以覆盖(override)一个static的方法?
    • 请列举你所知道的Object类的方法并简要说明。
    • 请说明类和对象的区别
    • 请你讲讲wait方法的底层原理

准备2020年春招的苦逼研究生的备战之第2版

Java SE

请你解释HashMap中为什么重写equals还要重写hashcode?

HashMap中,如果要比较key是否相等,要同时使用这两个函数!因为自定义的类的hashcode()方法继承于Object类,其hashcode码为默认的内存地址,这样即便有相同含义的两个对象,比较也是不相等的。HashMap中的比较key是这样的,先求出key的hashcode(),比较其值是否相等,若相等再比较equals(),若相等则认为他们是相等的。若equals()不相等则认为他们不相等。如果只重写hashcode()不重写equals()方法,当比较equals()时只是看他们是否为同一对象(即进行内存地址的比较),所以必定要两个方法一起重写。

解释:原来的hashcode使一个对象变成一个int型整数,JDK中hash()散列函数是对hashcode值进行2次散列处理(ipad-java1中提及),所以我们重写hashcode是为了让不同地址但是相同的含义的2个对象有相同的hashcode值,这样才会匹配到同一个关键码上(数据结构的散列提及),否则不重写的话,相同含义但地址不同的对象不会匹配成功。匹配到同一个关键码上,再用equals解决冲突问题,equals比较内容是否相同。

请你介绍一下map的分类和常见的情况

Map主要用于存储健值对,根据键得到值,因此不允许键重复(重复了覆盖了),但允许值重复。它是一个接口。它有四个实现类,分别是HashMap Hashtable LinkedHashMap 和TreeMap.

Hashmap 是一个最常用的Map,它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度,遍历时,取得数据的顺序是完全随机的。HashMap最多只允许一条记录的键为Null;允许多条记录的值为Null;HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致。如果需要同步,可以用Collections的synchronizedMap方法使HashMap具有同步的能力,或者使用ConcurrentHashMap。

Hashtable与 HashMap类似,它继承自Dictionary类,不同的是:它不允许记录的键或者值为空;它支持线程的同步,即任一时刻只有一个线程能写Hashtable,因此也导致了Hashtable在写入时会比较慢。

LinkedHashMap 是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的.也可以在构造时用带参数,按照应用次数排序。在遍历的时候会比HashMap慢,不过有种情况例外,当HashMap容量很大,实际数据较少时,遍历起来可能会比 LinkedHashMap慢,因为LinkedHashMap的遍历速度只和实际数据有关,和容量无关,而HashMap的遍历速度和他的容量有关。

TreeMap实现SortMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。
红黑二叉树的典型实现,且它会按照key递增的方式排序。

请你讲讲Java里面的final关键字是怎么用的?

  • 修饰变量:如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。

  • 修饰方法:final方法是子类中不能被覆盖的方法,但可以被重载。

  • 修饰类:表明这个类不能被继承。final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法。

请你谈谈关于Synchronized和lock

synchronized是Java的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。JDK1.5以后引入了自旋锁、锁粗化、轻量级锁,偏向锁来有优化关键字的性能。

Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

区别:

  • Lock是显式锁(手动开启和关闭锁),synchronized是隐式锁
  • Lock只有代码块锁,synchronized有代码块锁和方法锁
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,且具有很好的扩展性(提供很多子类)

优先使用顺序:Lock-同步代码块-同步方法

请你介绍一下volatile?

volatile关键字是用来保证有序性和可见性的。

我们所写的代码,不一定是按照我们自己书写的顺序来执行的happens-before机制
虚拟机JVM层面,为了尽可能减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影响,虚拟机会按照自己的一些规则将程序编写的顺序打乱,即写在后面的代码在时间顺序上会先执行,这样尽可能的利用CPU。例子:byte[] a = new byte[1024*1024](分配1M空间),它相对运行的很慢,会执行下面的boolean flag = true。
CPU硬件层面,CPU也会收到一批指令按照其规则重排序,同样是基于CPU速度比缓存速度快的原因,也会作类似处理。

如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这2个操作之间就存在数据依赖单线程不会出现问题,多线程可能会出现问题。如果重排序两个操作的执行顺序,程序的结果就改变了。

volatile保证了线程间变量的可见性,即后面线程能看到前面线程对变量A的改变。具体是符合一下两个规则

  • 线程对变量进行修改后,立刻回写到主内存;
  • 线程对变量进行读取的时候,要从主内存中读取,而不是缓存或工作内存。

可见性:首先Java内存模型分为,主内存,工作内存。比如线程A从主内存把变量从主内存读到了自己的工作内存中,做了加1的操作,但是此时没有将i的最新值刷新会主内存中,线程B此时读到的还是i的旧值。加了volatile关键字的代码生成的汇编代码发现,会多出一个lock前缀指令。

牛客上答案,看看而已:
happens-before规则,其中有条就是volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;有序性实现的是通过插入内存屏障来保证的。



请你介绍一下Syncronized锁,如果用这个关键字修饰一个静态方法,锁住了什么?如果修饰成员方法,锁住了什么?

synchronized修饰静态方法以及同步代码块的synchronized(类.class)用法锁的是类,线程想要执行对应同步代码,需要获得类锁。
synchronized修饰成员方法,线程获取的是当前调用该方法的对象实例的对象锁。

请解释Java中的概念,什么是构造函数?什么是构造函数重载?什么是复制构造函数?

当新对象被创建的时候,被自动调用的函数就是构造函数。每一个类都有构造函数。构造函数主要用来初始化对象。
特点:

  • 方法名和类名一致
  • 构造方法虽然有返回值,但是不能定义返回类型(即返回值的类型肯定是本类),不能在构造方法中调用return返回某个东西,但是可以仅仅写个return;代表方法的结束
  • 通过new关键字调用
  • 在程序员没有给类提供构造函数的情况下,Java编译器会为这个类创建一个默认的构造函数。

构造方法中使用this(能在构造方法使用this代表在调用构造方法之前,对象就已经被建好了):

  • 使用this来调用其他构造方法
  • 位置:必须是第一条语句

this不能用于static方法中:this对象的东西存在堆中,static存在方法区中。

Java中构造函数重载和方法重载很相似。可以为一个类创建多个构造函数。每一个构造函数必须有它自己唯一的参数列表。

如果对象中包含了对象等复杂类型,浅拷贝对象其实是对象的引用,而不是重新生成一个新副本。这时,如果对一个实例的内部类类型修改,其他实例的内部类类型也会被修改。这时就需要copy构造函数来进行深复制(也就是使对象中包含的类等复杂类型使用值赋值,而不是引用赋值)

拷贝构造方法就是用一个已经实例化的对象来new另一个对象

public User(User user) {

        this.name = user.name;

        this.inner = new Inner();

        inner.name = user.inner.name;

    }
    
调用:
User u2 = new User(u1);

Java clone、浅复制、深复制、复制构造函数的详细解释:https://blog.csdn.net/ShewMi/article/details/80788591

请说明Java中的方法覆盖(重写)(Overriding)和方法重载(Overloading)是什么意思?

Java中的方法重载发生在同一个类里面两个或者是多个方法的方法名相同但是参数不同(类型,个数,顺序不同)的情况。注意:只有返回值不同或只有形参的名字不同,不构成方法的重载。

与此相对,方法覆盖(重写)是说子类重新定义了父类的方法。方法覆盖必须有相同的方法名,参数列表和返回类型。

忽略:请说明Query接口(Hibernate)的list方法和iterate方法有什么区别?

①list()方法无法利用一级缓存和二级缓存(对缓存只写不读),它只能在开启查询缓存的前提下使用查询缓存;iterate()方法可以充分利用缓存,如果目标数据只读或者读取频繁,使用iterate()方法可以减少性能开销。
② list()方法不会引起N+1查询问题,而iterate()方法可能引起N+1查询问题

请你谈一下面向对象的"六原则一法则"。

  • 单一职责原则:一个类只做它该做的事情。(单一职责原则想表达的就是"高内聚",写代码最终极的原则只有六个字"高内聚、低耦合",所谓的高内聚就是一个代码模块只完成一项功能,在面向对象中,如果只让一个类完成它该做的事,而不涉及与它无关的领域就是践行了高内聚的原则,这个类就只有单一职责。另一个是模块化,好的自行车是组装车,从减震叉、刹车到变速器,所有的部件都是可以拆卸和重新组装的,好的乒乓球拍也不是成品拍,一定是底板和胶皮可以拆分和自行组装的,一个好的软件系统,它里面的每个功能模块也应该是可以轻易的拿到其他系统中使用的,这样才能实现软件复用的目标。)
  • 开闭原则:软件实体应当对扩展开放,对修改关闭。(在理想的状态下,当我们需要为一个软件系统增加新功能时,只需要从原来的系统派生出一些新类就可以,不需要修改原来的任何一行代码。要做到开闭有两个要点:①抽象是关键,一个系统中如果没有抽象类或接口系统就没有扩展点;②封装可变性,将系统中的各种可变因素封装到一个继承结构中,如果多个可变因素混杂在一起,系统将变得复杂而换乱,如果不清楚如何封装可变性,可以参考《设计模式精解》一书中对桥梁模式的讲解的章节。)
  • 依赖倒转原则:面向接口编程。(该原则说得直白和具体一些就是声明方法的参数类型、方法的返回类型、变量的引用类型时,尽可能使用抽象类型而不用具体类型,因为抽象类型可以被它的任何一个子类型所替代,请参考下面的里氏替换原则。)
  • 里氏替换原则:任何时候都可以用子类型替换掉父类型。(关于里氏替换原则的描述,Barbara Liskov女士的描述比这个要复杂得多,但简单的说就是能用父类型的地方就一定能使用子类型。里氏替换原则可以检查继承关系是否合理,如果一个继承关系违背了里氏替换原则,那么这个继承关系一定是错误的,需要对代码进行重构。例如让猫继承狗,或者狗继承猫,又或者让正方形继承长方形都是错误的继承关系,因为你很容易找到违反里氏替换原则的场景。需要注意的是:子类一定是增加父类的能力而不是减少父类的能力,因为子类比父类的能力更多,把能力多的对象当成能力少的对象来用当然没有任何问题。)-
  • 接口隔离原则:接口要小而专,绝不能大而全。(臃肿的接口是对接口的污染,既然接口表示能力,那么一个接口只应该描述一种能力,接口也应该是高度内聚的。例如,琴棋书画就应该分别设计为四个接口,而不应设计成一个接口中的四个方法,因为如果设计成一个接口中的四个方法,那么这个接口很难用,毕竟琴棋书画四样都精通的人还是少数,而如果设计成四个接口,会几项就实现几个接口,这样的话每个接口被复用的可能性是很高的。Java中的接口代表能力、代表约定、代表角色,能否正确的使用接口一定是编程水平高低的重要标识。)
  • 合成聚合复用原则:优先使用聚合或合成关系复用代码。(通过继承来复用代码是面向对象程序设计中被滥用得最多的东西,因为所有的教科书都无一例外的对继承进行了鼓吹从而误导了初学者,类与类之间简单的说有三种关系,Is-A关系、Has-A关系、Use-A关系,分别代表继承、关联和依赖。其中,关联关系根据其关联的强度又可以进一步划分为关联、聚合和合成,但说白了都是Has-A关系,合成聚合复用原则想表达的是优先考虑Has-A关系而不是Is-A关系复用代码,原因嘛可以自己从百度上找到一万个理由,需要说明的是,即使在Java的API中也有不少滥用继承的例子,例如Properties类继承了Hashtable类,Stack类继承了Vector类,这些继承明显就是错误的,更好的做法是在Properties类中放置一个Hashtable类型的成员并且将其键和值都设置为字符串来存储数据,而Stack类的设计也应该是在Stack类中放一个Vector对象来存储数据。记住:任何时候都不要继承工具类,工具是可以拥有并可以使用的,而不是拿来继承的。)
  • 迪米特法则:迪米特法则又叫最少知识原则,一个对象应当对其他对象有尽可能少的了解。再复杂的系统都可以为用户提供一个简单的门面,JavaWeb开发中作为前端控制器的Servlet或Filter不就是一个门面吗,浏览器对服务器的运作方式一无所知,但是通过前端控制器就能够根据你的请求得到相应的服务。调停者模式也可以举一个简单的例子来说明,例如一台计算机,CPU、内存、硬盘、显卡、声卡各种设备需要相互配合才能很好的工作,但是如果这些东西都直接连接到一起,计算机的布线将异常复杂,在这种情况下,主板作为一个调停者的身份出现,它将各个设备连接在一起而不需要每个设备之间直接交换数据,这样就减小了系统的耦合度和复杂度。

请说明如何通过反射获取和设置对象私有字段的值?

可以通过类对象的getDeclaredField()方法字段(Field)对象,然后再通过字段对象的setAccessible(true)将其设置为可以访问,接下来就可以通过get/set方法来获取/设置字段的值了。

请你谈谈如何通过反射创建对象?

  • 方法1:通过类对象调用newInstance()方法,例如:String.class.newInstance()
  • 方法2:通过类对象的getConstructor()或getDeclaredConstructor()方法获得构造器(Constructor)对象并调用其newInstance()方法创建对象,例如:String.class.getConstructor(String.class).newInstance("Hello");

内部类的详解

详细参考:https://www.cnblogs.com/dolphin0520/p/3811445.html

请说明内部类可以引用他包含类的成员吗,如果可以,有没有什么限制吗?

参考:https://www.cnblogs.com/wanglingling/p/7675918.html ,有代码演示

静态内部类:它是用static修饰的,在访问限制上它只能访问外部类中的static所修饰的成员变量或者是方法。

成员内部类:成员内部类是最普通的内部类,它可以无条件访问外部类的所有成员属性和成员方法(包括private成员和静态成员)。
(当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问的是成员内部类的成员。如果要访问外部类的同名成员,需要以下面的形式进行访问:
外部类.this.成员变量
外部类.this.成员方法)

局部内部类:局部内部类是定义在外部类的方法中的,在访问的时候它可以直接访问外部类的所有成员!但是不能随便访问局部变量,除非这个局部变量被final修饰。

匿名内部类: 匿名内部类其实就是局部内部类的简写格式,只能使用一次

内部类的修饰符?

成员内部类可以拥有private访问权限、protected访问权限、public访问权限及包访问权限。比如上面的例子,如果成员内部类Inner用private修饰,则只能在外部类的内部访问,如果用public修饰,则任何地方都能访问;如果用protected修饰,则只能在同一个包下或者继承外部类的情况下访问;如果是默认访问权限,则只能在同一个包下访问。这一点和外部类有一点不一样,外部类只能被public和包访问两种权限修饰。我个人是这么理解的,由于成员内部类看起来像是外部类的一个成员,所以可以像类的成员一样拥有多种权限修饰。

局部内部类就像是方法里面的一个局部变量一样,是不能有public、protected、private以及static修饰符的。

匿名内部类也是不能有访问修饰符和static修饰符的。

静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或者方法,这点很好理解,因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static成员就会产生矛盾,因为外部类的非static成员必须依附于具体的对象。

为什么成员内部类可以无条件访问外部类的成员?

参考博客的例子:

public class Outter {
    private Inner inner = null;
    public Outter() {
         
    }
     
    public Inner getInnerInstance() {
        if(inner == null)
            inner = new Inner();
        return inner;
    }
      
    //成员内部类
    protected class Inner {
        //无参构造器
        public Inner() {
             
        }
    }
}

编译器在进行编译的时候,会将成员内部类单独编译成一个字节码文件,即得到2个字节码文件:Outter.class,Outter$Inner.class。用反编译工具反编译之后,发现成员内部类有一个指向外部类对象的指针。

final com.cxh.test2.Outter this$0;

编译器会默认为成员内部类添加了一个指向外部类对象的引用,那么这个引用是如何赋初值的呢?下面接着看内部类的构造器:

public com.cxh.test2.Outter$Inner(com.cxh.test2.Outter);

从这里可以看出,虽然我们在定义的内部类的构造器是无参构造器,编译器还是会默认添加一个参数,该参数的类型为指向外部类对象的一个引用,所以成员内部类中的Outter this&0 指针便指向了外部类对象,因此可以在成员内部类中随意访问外部类的成员。从这里也间接说明了成员内部类是依赖于外部类的,如果没有创建外部类的对象,则无法对Outter this&0引用进行初始化赋值,也就无法创建成员内部类的对象了。

为什么局部内部类和匿名内部类只能访问局部final变量?(忽略)

例子:

public class Test {
    public static void main(String[] args)  {
         
    }
     
    public void test(final int b) {
        final int a = 10;
        new Thread(){
            public void run() {
                System.out.println(a);
            };
        }.start();
    }
}

当test方法执行完毕之后,变量a的生命周期就结束了,而此时Thread对象的生命周期很可能还没有结束,那么在Thread的run方法中继续访问变量a就变成不可能了,但是又要实现这样的效果,怎么办呢?Java采用了 复制的手段来解决这个问题。

如果这个变量的值在编译期间可以确定,则编译器默认会在匿名内部类(局部内部类)的常量池中添加一个内容相等的字面量或直接将相应的字节码嵌入到执行字节码中。这样一来,匿名内部类使用的变量是另一个局部变量,只不过值和方法中局部变量的值相等,因此和方法中的局部变量完全独立开。

也就说如果局部变量的值在编译期间就可以确定,则直接在匿名内部里面创建一个拷贝。如果局部变量的值无法在编译期间确定,则通过构造器传参的方式来对拷贝进行初始化赋值。

静态内部类有特殊的地方吗?

从前面可以知道,静态内部类是不依赖于外部类的,也就说可以在不创建外部类对象的情况下创建内部类的对象。另外,静态内部类是不持有指向外部类对象的引用的,这个读者可以自己尝试反编译class文件看一下就知道了,是没有Outter this&0引用的。

为什么在Java中需要内部类?

  • 每个内部类都能独立的继承一个接口的实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。内部类使得多继承的解决方案变得完整,
  • 方便将存在一定逻辑关系的类组织在一起,又可以对外界隐藏。

测试

根据注释填写(1),(2),(3)处的代码

public class Test{
    public static void main(String[] args){
           // 初始化Bean1
           (1)
           bean1.I++;
           // 初始化Bean2
           (2)
           bean2.J++;
           //初始化Bean3
           (3)
           bean3.k++;
    }
    class Bean1{
           public int I = 0;
    }
 
    static class Bean2{
           public int J = 0;
    }
}
 
class Bean{
    class Bean3{
           public int k = 0;
    }
}
  • 创建静态内部类对象的一般形式为: 外部类类名.内部类类名 xxx = new 外部类类名.内部类类名()
  • 创建成员内部类对象的一般形式为: 外部类类名.内部类类名 xxx = 外部类对象名.new 内部类类名()

因此,(1),(2),(3)处的代码分别为:

Test test = new Test();    
Test.Bean1 bean1 = test.new Bean1();  
Test.Bean2 b2 = new Test.Bean2();  
Bean bean = new Bean();     
Bean.Bean3 bean3 =  bean.new Bean3();   

一般来说,内部类是很少用来作为继承用的。但是当用来继承的话,要注意两点:

  • 成员内部类的引用方式必须为 Outter.Inner.
  • 构造器中必须有指向外部类对象的引用,并通过这个引用调用super()

如:

class WithInner {
    class Inner{
         
    }
}
class InheritInner extends WithInner.Inner { 
      
    // InheritInner() 是不能通过编译的,一定要加上形参 
    InheritInner(WithInner wi) { 
        wi.super(); //必须有这句调用
    } 
  
    public static void main(String[] args) { 
        WithInner wi = new WithInner(); 
        InheritInner obj = new InheritInner(wi); 
    } 
}

请说明JAVA语言如何进行异常处理,关键字:throws,throw,try,catch,finally分别代表什么意义?在try块中可以抛出异常吗?

Java 通过面向对象的方法进行异常处理,把各种不同的异常进行分类,并提供了良好的接口。
在Java中,每个异常都是一个对象,它是Throwable类或其它子类的实例。当一个方法出现异常后便抛出一个异常对象,该对象中包含有异常信息,调用这个对象的方法可以捕获到这个异常并进行处理。
Java的异常处理是通过5个关键词来实现的:try、catch、throw、throws和finally。
一般情况下是用try来执行一段程序,如果出现异常,系统会抛出(throws)一个异常,这时候你可以通过它的类型来捕捉(catch)它,或最后(finally)由缺省处理器来处理。用try来指定一块预防所有”异常”的程序。
紧跟在try程序后面,应包含一个catch子句来指定你想要捕捉的”异常”的类型。
throw语句用来明确地抛出一个”异常”。throws用来标明一个成员函数可能抛出的各种”异常”。
Finally为确保一段代码不管发生什么”异常”都被执行一段代码。可以在一个成员函数调用的外面写一个try语句,在这个成员函数内部写另一个try语句保护其他代码。每当遇到一个try语句,”异常“的框架就放到堆栈上面,直到所有的try语句都完成。如果下一级的try语句没有对某种”异常”进行处理,堆栈就会展开,直到遇到有处理这种”异常”的try语句。

请说明Java的接口和C++的虚类的相同和不同处。(即java的抽象类和接口的相同和不同之处)

C++虚类相当于java中的抽象类,问题简化成java的抽象类与接口的不同处是:

  • 一个子类只能继承一个抽象类,但能实现多个接口(在C++中,子类可以继承多个虚类)
  • 一个抽象类可以有构造方法,接口没有构造方法
  • 一个抽象类中的方法不一定是抽象方法,即其中的方法可以有实现(有方法体),接口中的方法都是抽象方法,不能有方法体,只有方法声明
  • 一个抽象类可以是public、private、protected、default,接口只有public
  • 一个抽象类中的方法可以是public、private、protected、default,接口中的方法只能是public和default修饰,实际上都是public的abstract方法
  • 相同之处是: 都不能实例化。

补充:C++ 子类继承父类纯虚函数、虚函数和普通函数的区别

1、虚函数:  
  C++的虚函数主要作用是“运行时多态”,父类中提供虚函数的实现,为子类提供默认的函数实现。子类可以重写父类的虚函数实现子类的特殊化。
2、纯虚函数:  
  C++中包含纯虚函数的类,被称为是“抽象类”。抽象类不能使用new出对象,只有实现了这个纯虚函数的子类才能new出对象。
  C++中的纯虚函数更像是“只提供申明,没有实现”,是对子类的约束,是“接口继承”。
  C++中的纯虚函数也是一种“运行时多态”。
3、普通函数:
  普通函数是静态编译的,没有运行时多态,只会根据指针或引用的“字面值”类对象,调用自己的普通函数。
  普通函数是父类为子类提供的“强制实现”。
  因此,在继承关系中,子类不应该重写父类的普通函数,因为函数的调用至于类对象的字面值有关

因此得出结论,纯虚函数和java的抽象类很相似,子类必须要强制实现。而c++的虚函数不需要子类强制实现。

抽象类和接口中方法方法修饰符问题

抽象类中的抽象方法(其前有abstract修饰)不能用private、static、synchronized、native访问修饰符修饰。
原因如下:
抽象方法没有方法体,是用来被继承的,所以不能用private修饰;
static修饰的方法可以通过类名来访问该方法(即该方法的方法体),抽象方法用static修饰没有意义;
使用synchronized关键字是为该方法加一个锁。而如果该关键字修饰的方法是static方法,则使用的锁 就是class变量的锁。如果是修饰类方法,则用this变量锁。但是抽象类不能实例化对象,因为该方法不是在该抽象类中实现的。是在其子类实现的。所以锁应该归其子类所有。所以抽象方 法也就不能用synchronized关键字修饰了;
native,这个东西本身就和abstract冲突,他们都是方法的声明,只是一个把方法实现移交 给子类,另一个是移交给本地操作系统。如果同时出现,就相当于即把实现移交给子类,又把实现移交给本地操作系统,那到底谁来实现具体方法呢?

接口是一种特殊的抽象类,接口中的方法全部是抽象方法(但其前的abstract可以省略),所以抽象类中的抽象方法不能用的访问修饰符这里也不能用。而且protected访问修饰符也不能使用,因为接口可以让所有的类去实现(非继承),不只是其子类,但是要用public去修饰。接口可以去继承一个已有的接口。

请说明一下final, finally, finalize的区别。

final 用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。
finally是异常处理语句结构的一部分,表示总是执行。
finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。

请说明面向对象的特征有哪些方面

1,封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。

2,继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。

3,多态:和java中的接口编程思想一致。如 List list = new ArrayList (); 多态性是允许将父对象(如:List)设置成为和一个或更多的他的子对象(如:ArrayList,LinkedList)相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针

请说明Comparable和Comparator接口的作用以及它们的区别。

Java提供了只包含一个compareTo()方法的Comparable接口。这个方法可以个给两个对象排序。具体来说,它返回负数,0,正数来表明输入对象小于,等于,大于已经存在的对象。

Java提供了包含compare()和equals()两个方法的Comparator接口。compare()方法用来给两个输入参数排序,返回负数,0,正数表明第一个参数是小于,等于,大于第二个参数。equals()方法需要一个对象作为参数,它用来决定输入参数是否和comparator相等。只有当输入参数也是一个comparator并且输入参数和当前comparator的排序结果是相同的时候,这个方法才返回true。

是否可以在static环境中访问非static变量?

1.不可以,因为static 变量属于类本身,在类加载的时候分配到方法区 也就是分配内存空间,
而 非static变量 必须在类new 的时候才会分配内存空间。

2.他们2个分配空间的时间是不一样的 java只有分配内存空间的 变量和方法 才能互相访问,
static变量在加载的时候就已经分配内存空间 而非static还没有分配内存空间,所以static 环境中是不能访问非static变量的,因为非static变量 这个时候在内存中是不存在的。

泛型

参考博客:https://www.jianshu.com/p/b5a8eef4aa38

请你讲讲什么是泛型?

泛型,即“参数化类型”。就是将类型由原来的具体的类型参数化,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

泛型的本质就是“数据类型的参数化”。我们可以把“泛型”理解为数据类型的一个占位符(形式参数),即告诉编译器,在调用泛型时必须传入实际类型。(类似于函数中定义了形参和调用时需要传入实参的概念)

请解释一下extends 和super 泛型限定符

extends上限通配符,用来限制类型的上限,只能传入本类和子类,add方法受阻(可以add(null)),可以从一个数据类型里获取数据;
super下限通配符,用来限制类型的下限,只能传入本类和父类,get方法受阻,可以把对象写入一个数据结构里;

在java泛型中,? 表示通配符,代表未知类型,< ? extends Object>表示上边界限定通配符,< ? super Object>表示下边界限定通配符。

import java.util.ArrayList;
import java.util.List;
  
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}
  
public class CovariantArrays {
  public static void main(String[] args) {
    //上界
    List<? extends Fruit> flistTop = new ArrayList<Apple>();
    flistTop.add(null);
    //add Fruit对象会报错
    //flist.add(new Fruit());
    Fruit fruit1 = flistTop.get(0);
  
    //下界
    List<? super Apple> flistBottem = new ArrayList<Apple>();
    flistBottem.add(new Apple());
    flistBottem.add(new Jonathan());
    //get Apple对象会报错
    //Apple apple = flistBottem.get(0);
  }
}

总结:

  • 限定通配符总是包括自己
  • 上界类型通配符:add方法受限
  • 下界类型通配符:get方法受限
  • 如果你想从一个数据类型里获取数据,使用 ? extends 通配符
  • 如果你想把对象写入一个数据结构里,使用 ? super 通配符
  • 如果你既想存,又想取,那就别用通配符
  • 不能同时声明泛型通配符上界和下界

泛型通配符 ? 与 T 的区别

参考博客:https://segmentfault.com/a/1190000020497160?utm_source=tag-newest

List , 这个 T 是一个形参,可以理解为一个占位符,被使用时,会在程序运行的时候替换成具体的类型,比如替换成String,Integer之类的。

List<?>, 这个 ? 是一个实参,这是Java定义的一种特殊类型,比Object更特殊,就像一个影子。比如List和List是没有父子关系的,这是两个类型,List类型和List类型;但是List<?> 是 List的父类。

用数学集合的语言来表述,?表示了集合【所有Java类型,String,Integer等系统定义的,或者用户定义的Foo等类型】这个整体;而T表示了集合【所有Java类型,String,Integer等系统定义的,或者用户定义的Foo等类型】中的一个成员。

正是因为 ? 是个集合,T是集合中的一个成员,导致我们很容易混淆这两者到底有什么区别。在一些情况下,这两者确实是可以相互替换的,但是另一种情况下就不行了。

?表示了任何的一种类型,那List<?>岂不是可以包含String和Integer,但这又和Java的类型系统矛盾了,List里面只能放一种类型。于是乎,对于 List<?> list 是不可能进行 list.add(1) 的,不能对它进行写操作,除了可以 list.add(null) 。

而对于 List, 却是可以进行写操作的:

public static <T extends Number> void addTExtend(List<T> list, T e){
        list.add(e);
    }

那List<?>到底有什么意义呢?在我不需要处理数组里的元素的时候,写代码更方便简单。如下两个方法的功能是一样的,但是使用List<?>简单些。

    public static void setNullWildcard(List<?> list){
        list = null;
    }
    
    public static <T> void setNullT(List<T> list){
        list = null;
    }

List<?>里面的元素,如果你取出来,会都被转化为Object(因为Object是这个集合里所有元素的父),如果你只需要用到这个集合最顶层的父元素的方法,比如List<?>就是Object的方法,List<? extends Number> 就是 Number 的方法,那你也可以使用List<?>以及List<? extends Number> 来简化代码的书写:

    public static <T> void printList(List<T> list){
        for(T e: list){
            System.out.print(e + " ");
        }
        System.out.println("");
    }

    public static void printListWildCard(List<?> list){
        for(Object e: list){
            System.out.print(e + " ");
        }
        System.out.println("");
    }

如上两个方法,功能都是一样的,只是用 List<?> 更简便。

总结 ? 相对于 T 的第一个区别:不关心List里面的元素,或者只需要用到List里面元素的最顶层父元素的方法的时候,可以用List<?>来简化代码的书写。

上面说到了 List<? extends Number>,如下代码,使用 ? 还是 T ,都可以实现同样的功能:

    public static void printListWildCardExtend(List<? extends Number> list){
        for(Number e: list){
            System.out.print(e + " ");
        }
        System.out.println("");
    }

    public static <T extends Number> void printListTExtend(List<T> list){
        for(Number e: list){
            System.out.print(e + " ");
        }
        System.out.println("");
    }

两者都可以通过extends来限定一个类型的子集,但是 T 可以 List<T extends Number & ExtendInterface> 即限定为多重继承的,? 却不可以:

    public static <T extends Number & ExtendInterface> void printListTExtend(List<T> list){
        for(Number e: list){
            System.out.print(e + " ");
        }
        System.out.println("");
    }

无法实现 void printListWildCardExtend(List<? extends Number & ExtendInterface> list)

总结 ? 与 T 的第二个区别:使用extends限定类型子集的时候,?不能多重继承,T 可以

    public static void printListSuperNumber(List<? super Integer> list){
        for(Object e: list) {
            System.out.print(e + " ");
        }
        System.out.println("");
    }

?是可以限定父集的,但是 T 是做不到这一点的。

总结?与 T的第三个区别:使用super限定父集的时候,? 可以, T 不可以

请你解释一下类加载机制,类加载器的作用,双亲委派模型,好处是什么?

类加载机制:JVM把class文件加载到内存,并对数据进行校验、解析和初始化,最终形成JVM可以直接使用的java类型的过程。

过程

  • 加载:将class字节码(即类的字节数组)内容加载到内存中,将静态数据转化成方法区中的运行时数据结构,在堆中生成一个代表这个类的Class对象,作为方法区类数据的访问入口。(这也是类加载器的作用
  • 链接:验证安全问题+为static变量分配方法区内存并设置类变量初始值阶段+类的final常量由符号引用到直接引用
  • 初始化:执行类的静态变量的赋值和静态块语句。

类的主动引用一定会发生类的初始化:

  • new一个类
  • 调用类的静态成员和方法,除了final修饰的
  • 反射机制
  • 初始化子类时,会先初始化它的父类

被动引用不会发生类的初始化:

  • 当访问一个静态域时,只有真正声明这个域的类才会被初始化(通过子类引用父类的静态变量,不会导致子类的初始化,会导致父类的初始化)
  • 引用常量时,不会初始化(常量在编译阶段就存入了类的常量池中了)

双亲委派模型:代理模式的特例,即父类加载器优先加载:某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次追溯,直到最高的父类,如果父类加载器能够加载类,则直接返回,否则才自己加载类。

好处:为了保证java核心库的类型安全。
例如:例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。

注意:tomcat服务器为了安全,它的类加载器首先尝试去加载某个类,如果找不到再代理给父类加载器。

双亲委派模型的缺点:

  • JDBC的API和SPI(Service provide Interface),API是核心库的一部分,由引导类加载器加载,而SPI由系统类加载器加载,所以有可能找不到SPI。

线程上下文类加载器:为了抛弃双亲委派加载链模式,新线程会继承父线程的上下文类加载器,即程序中所有的线程将都使用系统类加载器作为上下文类加载器。

请你谈谈StringBuffer和StringBuilder有什么区别,底层实现上呢?

StringBuffer线程安全,StringBuilder线程不安全,底层实现上的话,StringBuffer其实就是比StringBuilder多了Synchronized修饰符。

请说明String是否能继承?

不能被继承,因为String类有final修饰符,而final修饰的类是不能被继承的。

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    // 省略... 
}

请解释一下String为什么不可变?以及String不可变的好处有哪些?

不可变对象是指一个对象的状态在对象被创建之后就不再变化。不可改变的意思就是说:不能改变对象内的成员变量,包括基本数据类型的值不能改变,引用类型的变量不能指向其他的对象,引用类型指向的对象的状态也不能改变。

String 不可变是因为在 JDK 中 String 类被声明为一个 final 类,且类内部的 value 字节数组也是 final 的。

好处:

  • 只有当字符串是不可变时字符串池才有可能实现,字符串池的实现可以在运行时节约很多堆(heap) 空间,因为不同的字符串变量都指向池中的同一个字符串;
  • 如果字符串是可变的则会引起很严重的安全问题,譬如数据库的用户名密码都是以字符串的形式传入来获得数据库的连接,或者在socket编程中主机名和端口都是以字符串的形式传入,因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子改变字符串指向的对象的值造成安全漏洞;
  • 因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享,这样便不用因为线程安全问题而使用同步,字符串自己便是线程安全的;
  • 因为字符串是不可变的所以在它创建的时候hashcode就被缓存了,不变性也保证了hash码的唯一性,不需要重新计算,这就使得字符串很适合作为Map的键,字符串的处理速度要快过其它的键对象,这就是 HashMap 中的键往往都使用字符串的原因。

请说明”static”关键字是什么意思?Java中是否可以覆盖(override)一个static的方法?

“static”关键字表明一个成员变量或者是成员方法可以在没有所属的类的实例变量的情况下被访问。
Java中static方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而static方法是编译时静态绑定的。static方法跟类的任何实例都不相关,所以概念上不适用。

请列举你所知道的Object类的方法并简要说明。

Object()默认构造方法。
clone() 创建并返回此对象的一个副本。
equals(Object obj) 指示某个其他对象是否与此对象“相等”。
finalize()当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
getClass()返回一个对象的运行时类。
hashCode()返回该对象的哈希码值。
notify()唤醒在此对象监视器上等待的单个线程。
notifyAll()唤醒在此对象监视器上等待的所有线程。
toString()返回该对象的字符串表示。
wait()导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法。wait(long timeout)导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量。wait(long timeout, int nanos) 导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者其他某个线程中断当前线程,或者已超过某个实际时间量。

请说明类和对象的区别

区别:
 1,类是一个抽象的概念,类只是为所有的对象定义了抽象的属性与行为。
 2,对象是类的一个具体。
 3,类是一个静态的概念,当没有为类创建任何对象时,类本身不存在于内存空间中。
 4,对象是一个动态的概念。每一个对象都存在着有别于其它对象的属于自己的独特的属性和行为。对象的属性可以随着它自己的行为而发生改变。

联系:类是对象的抽象,而对象是类的具体实例。

请你讲讲wait方法的底层原理

参考博客:https://www.jianshu.com/p/6b9f260470a2

补充知识点:当有多个线程访问共享数据的时候,就需要对线程进行同步。线程中的几个主要方法的比较:
Thread类的方法:sleep(),yield()等
Object的方法:wait()和notify()等

Object 类中的 wait&notify 这两个方法包括他们的重载方法一共有 5 个,其中有 3 个方法是 native 的,也就是由虚拟机本地的 c 代码执行的。有 2 个 wait 重载方法最终还是调用了 wait(long) 方法。

  • wait方法:wait是要释放对象锁,进入等待池。既然是释放对象锁,那么肯定是先要获得锁。所以wait必须要写在synchronized代码块中,否则会报异常。
  • notify方法:也需要写在synchronized代码块中,调用对象的这两个方法也需要先获得该对象的锁。notify,notifyAll,唤醒等待该对象同步锁的线程,并放入该对象的锁池中。对象的锁池中线程可以去竞争得到对象锁,然后开始执行。
  • 如果是通过notify来唤起的线程,那先进入wait的线程会先被唤起来,并非随机唤醒;
  • 如果是通过nootifyAll唤起的线程,默认情况是最后进入的会先被唤起来,即LIFO的策略;
  • 另外一点比较重要,notify,notifyAll调用时并不会释放对象锁。比如以下代码:
public void test()
{
    Object object = new Object();
    synchronized (object){
        object.notifyAll();
        while (true){
         
        }
    }
}

虽然调用了notifyAll,但是紧接着进入了一个死循环。导致一直不能出临界区,一直不能释放对象锁。所以,即使它把所有在等待池中的线程都唤醒放到了对象的锁池中,但是锁池中的所有线程都不会运行,因为他们始终拿不到锁。


原文链接:https://www.cnblogs.com/baizihua/p/12066312.html
如有疑问请与原作者联系

标签:

版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有

上一篇:这一周股市价格为[2,6,1,4,8],求哪一天买入哪一天卖出,可获得

下一篇:面试官:请说一下对象锁和类锁的区别