Java基础面试题

面向对象

什么是面向对象?

对比面向过程,是两种不同的处理问题的角度,面向过程更注重事情的每一个步骤及顺序,面向对象更注重事情有哪些参与者(对象),及各自需要做什么,
面向过程比较直接高效,面向对象更容易复用,扩展和维护

封装

封装的意义,在于明确标识出允许外部使用的所有成员函数和数据项
内部细节对外部调用透明,外部调用无需修改或者关心内部实现

private String name;
public void setName(String name){
	this.name = "xq" + name;
}	
  • javabean的属性私有,提供get(),set()对外访问,因为属性的赋值或者获取逻辑只能由javabean本身决定,而不能由外部胡乱修改
  • orm框架
    mybatis

继承

继承基类的方法,并做出自己改变或扩展
子类共性的方法或者属性直接使用父类的,而不需要自己再定义,只需扩展自己个性化的

多态

基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同
继承,方法重写,父类引用指向子类对象

父类类型 变量名 = new 子类对象;
变量名.方法名()  

无法调用子类特有的功能

JDK,JRE,JVM三者区别和联系

JDK:Java Develpment Kit java开发工具
JRE:Java Runtime Environment java运行时环境
JVM:Java Virtual Machine java虚拟机
在这里插入图片描述

== 和 equals

== 对比的是栈中的值,基本数据类型是变量值,引用类型是堆中内存对象的地址

equals:object中默认也是==比较,通常会重写

 public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

final

简述final作用

  • 修饰类:表示类不可被继承
  • 修饰方法:表示方法不可被子类覆盖,但是可以重载
  • 修饰变量:表示变量一旦赋值就不可以更改它的值
  • 修饰成员变量
    • 如果final修饰的是类变量,只能在静态初始化块中指定初始值或者声明该类变量时指定初始值
    • 如果final修饰的是成员变量,可以在非静态初始化块,声明该变量或者构造器中执行初始值
  • 修饰局部变量
    系统不会为局部变量进行初始化,局部变量必须由程序员显式初始化,因此使用final修饰局部变量时,即可以在定义时指定默认值(后面的代码不能对变量再赋值),也可以不指定默认值,而在后面的代码中对final变量赋初值(仅一次)
  • 引用对象
    初始化后不能指向另一个地址,但内部值可以改变

为什么局部内部类和匿名内部类只能访问局部final变量

在java中,类是封装的,内部类也不例外。我们知道,非静态内部类能够访问外部类成员是因为它持有外部类对象的引用 Outer.this, 就像子类对像能够访问父类成员是持有父类对象引用super一样。局部内部类也和一般内部类一样,只持有了Outer.this,能够访问外部类成员,但是它又是如何访问到局部变量的呢?

实际上java是将局部变量作为参数传给了局部内部类的构造函数,而将其作为内部类的成员属性封装在了类中。我们看到的内部类访问局部变量实际上只是访问了自己的成员属性而已,这和类的封装性是一致的

public class TestInner {



    public static void main(String[] args) {

        Change c = method();

    }



    public static Change method(){

        int a = 10;

        class Inner implements Change{

            public void change(){

               // a = 20; //如果这里可以修改a的值

                System.out.println("a=" + a);//20

            }

        }

        Change in = new Inner();

        in.change();

        System.out.println("a="+a);

        //从阅读角度,

        //这里预期打印a=20,然而只会打印出a=10,

        //会让人误解,因为在Inner的change()中修改的是Inner内部类对象的this.a,

        //它是method的局部变量a的副本

        return in;

    }

}

interface Change{

    void change();

}

method()方法的局部变量a失效了,但其实change()方法已经访问的不是method()方法的局部变量a了,而是Inner内部类的成员变量a。
为了保证阅读与运行的一致性,只能规定局部变量a(和副本a)不能修改,这样阅读和运行就可保持一致。那么就只能限定a为final。

String,StringBuilder,StringBuffer

String是final修饰,不可变,每次操作都会产生新的String对象
StringBuffer和StringBuilder都是在原对象上操作
StringBuffer是线程安全的,StringBuilder线程不安全
StringBuffer方法都是synchronized修饰的
性能:StringBuilder > StringBuffer > String

场景:如果需要经常改变字符串内容使用StringBuffer,StringBuilder
优先使用StringBuilder,多线程使用共享变量时使用StringBuffer

重载和重写的区别

重载:发生在同一个类中,方法名必须相同,参数类型不同,个数不同,顺序不同,方法返回值和访问修饰符可以不同,发生在编译时
重写:发生在父子类中,方法名,参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类(多态),如果父类方法访问修饰符为private则子类就不能重写该方法

接口和抽象类的区别

  • 抽象类可以存在普通成员函数,而接口中只能存在public abstract方法(default方法)
  • 抽象类中的成员变量可以是多种类型的,接口中成员变量只能是public static final的
  • 抽象类只能继承一个,接口可以实现多个
interface i{
     default void test(){
         System.out.println("aaaaaaaaa");
     }
     static void test2(){
         System.out.println("bbbbbbbbbbbbb");
     }
}

接口设计的目的,是对类的行为进行约束,是对行为的抽象 like a
抽象类大的设计目的,是代码复用,是对类本质的抽象 is a
例子:Servlet — HttpServlet

List和Set的区别

  • List:有序,按对象进入的顺序保存对象,可重复,允许多个Null元素对象,可以使用Iterator取出所有元素,再逐一遍历,还可以使用get(int index)获取指定下标的元素

  • Set 无需,不可重复,最多允许有一个Null元素的对象,取元素时,只能用Iterator接口取得所有的元素,再逐一遍历各个元素

ArrayList和LinkedList区别(重要)

  • ArrayList:基于动态数组,连续内存存储,适合下标访问(随机访问),扩容机制(0.75,1.5倍):因为数组长度固定,超出长度存数据时需要新建数组,然后将老数组的数据拷贝到新数组,如果不是尾部插入数据还会涉及到元素的移动(往后复制一份,插入新元素),使用尾插法并指定初始容量可以极大提升性能,甚至超过LinkedList(需要创建大量node对象)
  • LinkedList:基于链表,可以存储在分散的内存中,适合做数据插入及删除操作,不适合查询,需要逐一遍历
  • 遍历LinkedList必须使用iterator不能使用for循环,因为每次for循环体内通过get(i)取得某一元素时都需要堆list重新遍历,性能消耗极大

HashMap和ConcurrentHashMap的区别,底层实现是什么(重要)

  1. 线程是否安全: HashMap 是非线程安全的,HashTable 是线程安全的,因为 HashTable 内部的方法基本都经过synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
  2. 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;
    对 Null key 和 Null value 的支持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,否则会抛出 NullPointerException。
  3. 初始容量大小和每次扩充容量大小的不同 : ① 创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小(HashMap 中的tableSizeFor()方法保证,下面给出了源代码)。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。
  4. 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制

在这里插入图片描述

ConcurrentHashMap原理,jdk7和8有什么区别

jdk7:

数据结构:ReentrantLock+Segment+HashEntry,一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构
在这里插入图片描述

元素查询:二次hash,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部
锁:Segment分段锁,Segment继承了ReentranLock,锁定操作的Segment.其他的Segment不受影响,并发度为Segment个数,可以通过构造函数指定,数组扩容不会影响其他的Segment
get方法无需加锁,volatile保证

jdk8:

在这里插入图片描述

数据结构:synchronized(数组扩容)+CAS+Node+红黑树,Node的val和next都用volatile修饰,保证可见性
查找,替换,赋值操作都使用CAS
锁:锁链表的head节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作,并发扩容
读操作无锁
Node的val和next用valatile修饰,读写线程对该变量互相可见
数组用volatile修饰,保证扩容时被读线程感知

Java中的异常体系

Java中的所有异常类都来自顶级父类Throwable
Throwable下有两个子类Exception和Error
Error是程序无法处理的错误,一旦出现这个错误,则程序将被迫停止运行
Exception不会导致程序停止,又分为两个部分RunTimeException运行时异常和CheckedException检查异常
RunTimeException常常发生在程序运行过程中,会导致程序当前线程执行失败,CheckedException常常发生在程序编译过程中,会导致程序编译不通过

在这里插入图片描述

Java中的引用传递

一文彻底搞懂Java中的值传递和引用传递!

java中是值传递,基本数据类型传递值,引用类型传递地址的值

HashMap

hashCode与equals

hashCode介绍

hashCode()的作用是获取哈希码,native方法,返回一个int整数,这个哈希码的作用是确定该对象在哈希表中的索引位置,hashCode()定义在JDK的Object.java中,Java中的任何类都包含由hashCode()函数,散列表存储的是键值对(key-value),它的特点是:能根据"键"快速的检索出对应的"值",这其中就利用到了散列码

为什么要有hashCode

以"HashSet如何检查重复"为例
对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,看该位置是否有值,如果没有,HashSet会假设对象没有重复出现,如果有值,则会调用equals方法检查是否真的相同,如果相同,就不加入,如果不同,链表(红黑树)

  • 如果两个对象相等,则hashcode相同
  • 两个对象相等,对两个对象分别调用equals方法返回true
  • 两个对象有相同的hashcode值,它们也不一定时相等的
  • 因此,equals方法被覆盖过,则hashcode方法也必须被覆盖
  • hashcode的默认行为是对堆上的对象产生独特值,如果没有重写hashcode,则该class的两个对象无论如何不会相等(即使两个对象数据相同)

为什么重写了equals方法同时也需要重写hash?

为了保证上述规则的实现,否则hashCode没有意义

说一下HashMap的put方法

  1. 根据key通过哈希算法与与运算得到数组下标
  2. 如果数组下标位置元素为空,则将key,value封装为Entry对象,(JDK1.7中为Entry对象,JDK1.8中为Node对象)并放入该位置
  3. 如果数组下标位置元素不为空,则要分情况讨论
    • 如果是JDK1.7.则先判断是否需要扩容,如果要扩容就进行扩容,如果不用扩容就生成Entry对象,并使用头插法添加到当前位置的链表中
    • 如果是JDK1.8,则会先判断当前位置上的Node的类型,看是红黑树Node还是链表Node
      • 如果是红黑树Node,则将key和value封装成一个红黑数节点添加到红黑树中去,在这个过程中会判断红黑树中否存在当前key,如果存在则更新value
      • 如果此位置上的Node对象是链表节点,则将key和value封装为一个链表Node并通过尾插法插入到链表的最后位置去,因为是尾插法,所以需要遍历链表,在遍历链表的过程中会判断是否存在当前key,如果存在则更新value,当遍历完链表后,将新链表Node插入到链表中,插入到链表后,会看当前链表的节点个数,如果超过了8且数组长度超过64,则会将该链表转化为红黑树
      • 将key和value封装为Node插入到链表或红黑树中后,再判断是否需要进行扩容,如果需要就扩容,如果不需要就结束put方法

说说HashMap中的hash算法

HashMap中的hash算法实现:

把高位的hashCode()向低位移动(数组长度一般都小于2^16即65536)
为什么用^?因为 & 和 |都会使运算结果偏向0和1

static final int hash(Object key) {
       int h;
       //扰动函数
       return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  }


//&运算比%快很多
int n=tab.length-1
int index = (n - 1) & hash;

为什么HashMap的长度最好是2的幂次方?

hash算法中用HashMap长度-1对hash值进行与运算,如果HashMap的长度为2的幂次方,二进制上n - 1每个位置都为1,在计算index时,可以完全取决于hash的值,更不容易发生hash碰撞

&运算比%快很多,HashMap的长度用了2的n次方,可以使用&来求位置,获取更好的性能

如何避免HashMap内存泄漏?

重写equals方法

HashMap构造

HashMap在构造时并不会创建数组,在put元素时才创建(putVal()中的resize())

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值