java面试必知必会

面向对象

什么是面向对象?

  • 两种主流开发方法,结构化开发和面向对象开发
  • 基本思想是使用类、对象、继承、封装、消息等基本概念进行程序设计
  • 面向对象的3大特征,封装,继承,多态
  • 更好的可重用性,可扩展性和可维护性
  • 面向对象基本单位是类 成员变量 + 方法 = 类

成员变量成员方法

概念区别

Integer相关

	Integer a = 1;
	Integer b = 2;
	Integer c = 3;
	Integer d = 3;
	Integer e = 321;
	Integer f = 321;
	Long g = 3L;
	System.out.println(c == d);// true
	System.out.println(e == f);//false
	System.out.println(c == (a+b));//true
	System.out.println(c.equals(a+b));//true
	System.out.println(g == (a+b));//true
	System.out.println(g.equals(a+b));//false

double 和 Double相关

		double i = 0.0/0.0;System.out.println(i==i); //false     
		double i2 = 1.0/0.0;System.out.println(i2==i2);//true
	
		Double di = 0.0/0.0; System.out.println(di == di);	//true	
		Double di2 = 1.0/0.0; System.out.println(di2 == di2);//true

多态,向上转型

  • 相同类型的变量、调用同一个方法时呈现出多种不同的行为特征,这就是多态,编译时类型和运行时类型不一致,编译看左边运行看右边
  • 把子类对象赋值给父类引用时,称为向上转型。这种转型只是表明这个引用变量的编译是类型是父类,但是实际执行他的方法时,依然表现出子类对象的行为方式。但是把一个父类对象赋值给子类引用变量时,就需要强制类型转换,而且还可能在运行时产生ClassCastException异常。

hashcode、==、equals比较

java中equals,hashcode和==的区别

java中子类继承父类时是否继承构造函数

  • 构造函数不能继承,只是调用而已。如果父类没有无参构造函数创建子类时,不能编译,除非在构造函数代码体中第一行,必须是第一行显式调用父类有参构造函数,如果不显示调用父类有参构造函数,系统会默认调用父类无参构造函数super();但是父类中没有无参构造函数,那它不是不能调用了。所以编译就无法通过了

public、protect、private、static、final、abstract

关键字 static、final、this、super

static:关键字,是一个修饰符,用于修饰成员(成员变量和成员函数)

  • 特点:
    1、想要实现对象中的共性数据的对象共享。可以将这个数据进行静态修饰
    2、被静态修饰的成员,可以直接被类名所调用。也就是说,静态的成员多了一种调用方式。类名.静态方式。
    3、静态随着类的加载而加载。而且优先于对象存在。
    4、static修饰内部类,普通类是不允许声明为静态的,只有内部类才可以
  • 弊端:
    1、有些数据是对象特有的数据,是不可以被静态修饰的。因为那样的话,特有数据会变成对象的共享数据。这样对事物的描述就出了问题。所以,在定义静态时,必须要明确,这个数据是否是被对象所共享的。
    2、静态方法只能访问静态成员,不可以访问非静态成员。(即static修饰符不能访问没有是static修饰的成员)因为静态方法加载时,优先于对象存在,所以没有办法访问对象中的成员。
    3、静态方法中不能使用this,super关键字.因为this代表对象,而静态在时,有可能没有对象,所以this无法使用

this:关键字

  • java中提供了this关键字,this关键字总是指向调用该方法的对象
  • this可以代表任何对象,当this出现在某个方法体中的时候,他所代表的对象是不确定的,但是他的类型是确定的,他所代表的对象只能是当前类;只有当这个方法被调用时,他所代表的对象才能被确定下来,谁在调用这个方法,this就代表谁。

1、调用本类方法

 public String introYourself() {
        return this.whoAreU() + this.haoOldAreU();
    }

2、调用本类属性

   public void changeMyName(String name) {
        this.name = name;
    }

3、调用本类的其他构造方法

  public UserExample(String name) {
        this(name, -1);
    }

4、调用父类的或指定的其他的类的同名方法,为避免歧义而生的方法

   public String whoAreSuper() {
            return "super is " + UserExample.this.whoAreU() + ". ";
        }

5、隐藏式的调用,为了写代码方便(更常用),不指定范围,java会在全类范围内向上查找变量或方法

      public String whoAmI() {
            return whoAreU();
        }

类的加载过程验证

public class StaticTest2 {
	public static void main(String[] args){
        new B();
    }
    static class A{
        int a = 3;
	    static {   //静态代码块
	        System.out.println("父类A静态代码块");
	    }
	    {       // 实例代码块

	        System.out.println("父类A实例代码块");
	    }

        public A(){
            System.out.println("父类A的构造器 "+a);
            a = 2;
            display();
        }
        public A(int i){
        	System.out.println("父类A的带参构造函数"+i);
        }

        public void display(){
            System.out.println("父类A的实例函数: "+a);
        }
    }
    static class B extends A{
        static int a = 1;
	    static {   //静态代码块
	        System.out.println("子类B静态代码块");
	    }
	    {       // 实例代码块

	        System.out.println("子类B实例代码块");
	    }
        public B(){
            super();        	
            System.out.println("子类B的默认构造函数: "+a);
            a = 5;
            display();
        }
        public  B(int j){
        	System.out.println("子类B的带参构造函数"+j);			
		}
        @Override
        public void display(){
            System.out.println("子类B的实例函数 "+a);
        }
    }
}	

输出结果:
输出结果

一个实例变量在对象初始化的过程中会被赋值几次?

  • 我们知道,JVM在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个时候实例变量被第一次赋值,这个赋值过程是没有办法避免的。如果我们在声明实例变量x的同时对其进行了赋值操作,那么这个时候,这个实例变量就被第二次赋值了。如果我们在实例代码块中,又对变量x做了初始化操作,那么这个时候,这个实例变量就被第三次赋值了。如果我们在构造函数中,也对变量x做了初始化操作,那么这个时候,变量x就被第四次赋值。也就是说,在Java的对象初始化过程中,一个实例变量最多可以被初始化4次

实例化的过程

  • 在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到Object类。此时,首先实例化Object类,再依次对以下各类进行实例化,直到完成对目标类的实例化。具体而言,在实例化每个类时,都遵循如下顺序:先依次执行实例变量初始化和实例代码块初始化,再执行构造函数初始化。也就是说,编译器会将实例变量初始化和实例代码块初始化相关代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后,构造函数本身的代码之前
    在这里插入图片描述
  • 类实例化的一般过程是:父类的类构造器<clinit>() -> 子类的类构造器<clinit>() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数
    

参考文章:JVM类生命周期概述:加载时机与加载过程

public class StaticTest {
    public static void main(String[] args) {
        staticFunction();
    }

    static StaticTest st = new StaticTest();

    static {   //静态代码块
        System.out.println("1");
    }

    {       // 实例代码块
        System.out.println("2");
    }

    StaticTest() {    // 实例构造器
        System.out.println("3");
        System.out.println("a=" + a + ",b=" + b);
    }

    public static void staticFunction() {   // 静态方法
        System.out.println("4");
    }

    int a = 110;    // 实例变量
    static int b = 112;     // 静态变量
}/* Output: 
		2
        3
        a=110,b=0
        1
        4
 *///:~

对比 Vector、ArrayList、LinkedList 有何区别

  • 这三者都是实现集合框架中的 List,也就是所谓的有序集合,因此具体功能也比较近似,比如都提供按照位置进行定位、添加或者删除的操作,都提供迭代器以遍历其内容等。但因为具体的设计区别,在行为、性能、线程安全等方面,表现又有很大不同
  • Vector 是 Java 早期提供的线程安全的动态数组,如果不需要线程安全,并不建议选择,毕竟同步是有额外开销的。Vector 内部是使用对象数组来保存数据,可以根据需要自动的增加容量,当数组已满时,会创建新的数组,并拷贝原有数组数据
  • ArrayList 是应用更加广泛的动态数组实现,它本身不是线程安全的,所以性能要好很多。与Vector 近似,ArrayList 也是可以根据需要调整容量,不过两者的调整逻辑有所区别,Vector 在扩容时会提高 1 倍,而 ArrayList 则是增加 50%
  • LinkedList 顾名思义是 Java 提供的双向链表,所以它不需要像上面两种那样调整容量,它也不是线程安全的
    set集合

Hashtable、HashMap、TreeMap 有什么不同

  • Hashtable、HashMap、TreeMap 都是最常见的一些 Map 实现,是以键值对的形式存储和操作数据的容器类型
  • Hashtable 是早期 Java 类库提供的一个哈希表实现,本身是同步的,不支持 null 键和值,由于同步导致的性能开销,所以已经很少被推荐使用
  • HashMap 是应用更加广泛的哈希表实现,行为上大致上与 HashTable 一致,主要区别在于HashMap 不是同步的,支持 null 键和值等。通常情况下,HashMap 进行 put 或者 get操作 可以达到常数时间的性能,所以它是绝大部分利用键值对存取场景的首选,比如,实现一个用户ID 和用户信息对应的运行时存储结构
  • TreeMap 则是基于红黑树的一种提供顺序访问的 Map,和 HashMap 不同,它的get、put、remove 之类操作都是 O(log(n))的时间复杂度,具体顺序可以由指定的Comparator 来决定,或者根据键的自然顺序来判断
  • 大部分使用 Map 的场景,通常就是放入、访问或者删除,而对顺序没有特别要求,HashMap在这种情况下基本是最好的选择。HashMap 的性能表现非常依赖于哈希码的有效性,请务必掌握 hashCode 和 equals 的一些基本约定:
    •  equals 相等,hashCode 一定要相等,重写了 hashCode 也要重写 equals,hashCode 需要保持一致性,状态改变返回的哈希值仍然要一致,equals 的对称、反射、传递等特性
      
  • LinkedHashMap 通常提供的是遍历顺序符合插入顺序它的实现是通过为条目(键值对)维护一个双向链表
  • 对于 TreeMap,它的整体顺序是由键的顺序关系决定的,通过 Comparator 或Comparable(自然顺序)来决定
    Map集合整体架构

Hashtable、HashMap、TreeMap心得

三者均实现了Map接口,存储的内容是基于key-value的键值对映射,一个映射不能有重复的键,一个键最多只能映射一个值

  • 元素特性
    • HashTable中的key、value都不能为null;HashMap中的key、value可以为null,很显然只能有一个key为null的键值对,但是允许有多个值为null的键值对;TreeMap中当未实现 Comparator 接口时,key 不可以为null;当实现 Comparator 接口时,若未对null情况进行判断,则key不可以为null,反之亦然
  • 顺序特性
    • HashTable、HashMap具有无序特性。TreeMap是利用红黑树来实现的(树中的每个节点的值,都会大于或等于它的左子树种的所有节点的值,并且小于或等于它的右子树中的所有节点的值),实现了SortMap接口,能够对保存的记录根据键进行排序。所以一般需要排序的情况下是选择TreeMap来进行,默认为升序排序方式(深度优先搜索),可自定义实现Comparator接口实现排序方式
  • 初始化与增长方式
    • 初始化时:HashTable在不指定容量的情况下的默认容量为11,且不要求底层数组的容量一定要为2的整数次幂;HashMap默认容量为16,且要求容量一定为2的整数次幂。
      扩容时:Hashtable将容量变为原来的2倍加1;HashMap扩容将容量变为原来的2倍
  • 线程安全性
    • HashTable其方法函数都是同步的(采用synchronized修饰),不会出现两个线程同时对数据进行操作的情况,因此保证了线程安全性。也正因为如此,在多线程运行环境下效率表现非常低下。因为当一个线程访问HashTable的同步方法时,其他线程也访问同步方法就会进入阻塞状态。比如当一个线程在添加数据时候,另外一个线程即使执行获取其他数据的操作也必须被
      阻塞,大大降低了程序的运行效率,在新版本中已被废弃,不推荐使用。
      HashMap不支持线程的同步,即任一时刻可以有多个线程同时写HashMap;可能会导致数据的不一致。如果需要同步
      (1)可以用 Collections的synchronizedMap方法;
      (2)使用ConcurrentHashMap类,相较于HashTable锁住的是对象整体,ConcurrentHashMap基于lock实现锁分段技术。首先将Map存放的数据分成一段一段的存储方式,然后给每一段数据分配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。ConcurrentHashMap不仅保证了多线程运行环境下的数据访问安全性,而且性能上有长足的提升
  • 一段话HashMap
    • HashMap基于哈希思想,实现对数据的读写。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。当两个不同的键对象的hashcode相同时,它们会储存在同一个bucket位置的链表中,可通过键对象的equals()方法用来找到键值对。如果链表大小超过阈值(TREEIFY_THRESHOLD, 8),链表就会被改造为树形结构

如何保证容器是线程安全的?ConcurrentHashMap 如何实现高效地线程安全?

  • Java 提供了不同层面的线程安全支持。在传统集合框架内部,除了 Hashtable 等同步容器,还提供了所谓的同步包装器(Synchronized Wrapper),我们可以调用 Collections 工具类提供的包装方法,来获取一个同步的包装容器(如 Collections.synchronizedMap),但是它们都是利用非常粗粒度的同步方式,在高并发情况下,性能比较低下。
    另外,更加普遍的选择是利用并发包提供的线程安全容器类,它提供了:
    • 各种并发容器,比如 ConcurrentHashMap、CopyOnWriteArrayList。
    • 各种线程安全队列(Queue/Deque),如 ArrayBlockingQueue、SynchronousQueue。
    • 各种有序容器的线程安全版本等
      具体保证线程安全的方式,包括有从简单的 synchronize 方式,到基于更加精细化的,比如基于分离锁实现的 ConcurrentHashMap 等并发实现等

Java 提供了哪些 IO 方式? NIO 如何实现多路复用?

Java IO 方式有很多种,基于不同的 IO 抽象模型和交互方式,可以进行简单区分。

  • 首先,传统的 java.io 包,它基于流模型实现,提供了我们最熟知的一些 IO 功能,比如 File 抽象、输入输出流等。交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序
    • java.io 包的好处是代码比较简单、直观,缺点则是 IO 效率和扩展性存在局限性,容易成为应用性能的瓶颈。
    • 很多时候,人们也把 java.net 下面提供的部分网络 API,比如 Socket、ServerSocket、HttpURLConnection 也归类到同步阻塞 IO 类库,因为网络通信同样是 IO 行为
  • 第二,在 Java 1.4 中引入了 NIO 框架(java.nio 包),提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层的高性能数据操作方式
  • 第三,在 Java 7 中,NIO 有了进一步的改进,也就是 NIO 2,引入了异步非阻塞 IO 方式,也有很多人叫它 AIO(Asynchronous IO)。异步 IO 操作基于事件和回调机制,可以简单理解为,应用操作直接返回,而不会阻塞在那里,当后台处理完成,操作系统会通知相应线程进行后续工作
  • IO 都是同步阻塞模式,所以需要多线程以实现多任务处理。而NIO 则是利用了单线程轮询事件的机制,通过高效地定位就绪的 Channel
    在这里插入图片描述

Java 有几种文件拷贝方式?哪一种最高效?

*Java 有多种比较典型的文件拷贝实现方式,例如利用 java.io 类库,直接为源文件构建一个 FileInputStream 读取,然后再为目标文件构建一个 FileOutputStream,完成写入工作

public static void copyFileByStream(File source, File dest) throws
        IOException {
    try (InputStream is = new FileInputStream(source);
         OutputStream os = new FileOutputStream(dest);){
        byte[] buffer = new byte[1024];
        int length;
        while ((length = is.read(buffer)) > 0) {
            os.write(buffer, 0, length);
        }
    }
 }

或者,利用 java.nio 类库提供的 transferTo 或 transferFrom 方法实现

public static void copyFileByChannel(File source, File dest) throws
        IOException {
    try (FileChannel sourceChannel = new FileInputStream(source)
            .getChannel();
         FileChannel targetChannel = new FileOutputStream(dest).getChannel
                 ();){
        for (long count = sourceChannel.size() ;count>0 ;) {
            long transferred = sourceChannel.transferTo(
                sourceChannel.position(), count, targetChannel);
                sourceChannel.position(sourceChannel.position() + transferred);
            	count -= transferred;
        }
    }
 }

对于 Copy 的效率,这个其实与操作系统和配置等情况相关,,总体上来说,NIO transferTo/From 的方式可能更快,因为它更能利用现代操作系统底层机制,避免不必要拷贝和上下文切换

谈谈接口和抽象类有什么区别?

  • 接口和抽象类是 Java 面向对象设计的两个基础机制
  • 接口是对行为的抽象,它是抽象方法的集合,利用接口可以达到 API 定义和实现分离的目的。接口,不能实例化;不能包含任何非常量成员,任何属性都是隐含着 public static final 的意义;同时,没有非静态方法实现,也就是说要么是抽象方法,要么是静态方法。Java 标准类库中,定义了非常多的接口,比如 java.util.List
  • 抽象类是不能实例化的类,用 abstract 关键字修饰 class,其目的主要是代码重用。除了不能实例化,形式上和一般的 Java 类并没有太大区别,可以有一个或者多个抽象方法,也可以没有抽象方法。抽象类大多用于抽取相关 Java 类的共用方法实现或者是共同成员变量,然后通过继承的方式达到代码复用的目的。Java 标准库中,比如 collection 框架,很多通用部分就被抽取成为抽象类,例如 java.util.AbstractList。
  • Java 类实现 interface 使用 implements 关键词,继承 abstract class 则是使用 extends 关键词,我们可以参考 Java 标准库中的 ArrayList

面向对象设计

封装、继承、多态

  • 封装的目的是隐藏事务内部的实现细节,以便提高安全性和简化编程。封装提供了合理的边界,避免外部调用者接触到内部的细节。我们在日常开发中,因为无意间暴露了细节导致的难缠 bug太多了,比如在多线程环境暴露内部状态,导致的并发修改问题。从另外一个角度看,封装这种隐藏,也提供了简化的界面,避免太多无意义的细节浪费调用者的精力。
  • 继承是代码复用的基础机制,类似于我们对于马、白马、黑马的归纳总结。但要注意,继承可以看作是非常紧耦合的一种关系,父类代码修改,子类行为也会变动。在实践中,过度滥用继承,可能会起到反效果
  • 你可能立即会想到重写(override)和重载(overload)、向上转型。简单说,重写是父子类中相同名字和参数的方法,不同的实现;重载则是相同名字的方法,但是不同的参数,本质上这些方法签名是不一样的

面向对象编程,掌握基本的设计原则S.O.L.I.D 原则

  • 单一职责(Single Responsibility),类或者对象最好是只有单一职责,在程序设计中如果发现某个类承担着多种义务,可以考虑进行拆分。
  • 开关原则(Open-Close, Open for extension, close for modification),设计要对扩展开放,对修改关闭。换句话说,程序设计应保证平滑的扩展性,尽量避免因为新增同类功能而修改已有实现,这样可以少产出些回归(regression)问题
  • 里氏替换(Liskov Substitution),这是面向对象的基本要素之一,进行继承关系抽象时,凡是可以用父类或者基类的地方,都可以用子类替换
  • 接口分离(Interface Segregation),我们在进行类和接口设计时,如果在一个接口里定义了太多方法,其子类很可能面临两难,就是只有部分方法对它是有意义的,这就破坏了程序的内聚性。
    对于这种情况,可以通过拆分成功能单一的多个接口,将行为进行解耦。在未来维护中,如果某个接口设计有变,不会对使用其他接口的子类构成影响
  • 依赖反转(Dependency Inversion),实体应该依赖于抽象而不是实现。也就是说高层次模块,不应该依赖于低层次模块,而是应该基于抽象。实践这一原则是保证产品代码之间适当耦合度的法宝。

谈谈你知道的设计模式?请手动实现单例模式,Spring 等框架中使用了哪些模式?

大致按照模式的应用目标分类,设计模式可以分为创建型模式、结构型模式和行为型模式

  • 创建型模式,是对对象创建过程的各种问题和解决方案的总结,包括各种工厂模式(Factory、Abstract Factory)、单例模式(Singleton)、构建器模式(Builder)、原型模式(ProtoType)
  • 结构型模式,是针对软件设计结构的总结,关注于类、对象继承、组合方式的实践经验。常见的结构型模式,包括桥接模式(Bridge)、适配器模式(Adapter)、装饰者模式(Decorator)、代理模式(Proxy)、组合模式(Composite)、外观模式(Facade)、享元模式(Flyweight)等
  • 行为型模式,是从类或对象之间交互、职责划分等角度总结的模式。比较常见的行为型模式有策略模式(Strategy)、解释器模式(Interpreter)、命令模式(Command)、观察者模式(Observer)、迭代器模式(Iterator)、模板方法模式(Template Method)、访问者模式(Visitor)

synchronized 底层如何实现?什么是锁的升级、降级?

  • synchronized 代码块是由一对儿monitorenter/monitorexit 指令实现的,Monitor 对象是同步的基本实现单元
  • 在 Java 6 之前,Monitor 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作。
  • 现代的(Oracle)JDK 中,JVM提供了三种不同的 Monitor 实现,也就是常说的三种不同的锁:偏斜锁(Biased Locking)、轻量级锁和重量级锁,大大改进了其性能。
  • 所谓锁的升级、降级,就是 JVM 优化 synchronized 运行的机制,当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。
  • 当没有竞争出现时,默认会使用偏斜锁。JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。
  • 如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM 就需要撤销(revoke)偏斜锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁

免责声明

本文章是作者查阅java面试资料而进行总结汇总的JAVA面试必知必会,如有侵权请联系作者删除

发布了3 篇原创文章 · 获赞 1 · 访问量 1992
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 技术黑板 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览