0
点赞
收藏
分享

微信扫一扫

【java基础】面试

一点读书 2022-04-05 阅读 68
java

文章目录

二叉树

二叉树遍历

前序遍历:根结点 —> 左子树 —> 右子树
中序遍历:左子树—> 根结点 —> 右子树
后序遍历:左子树 —> 右子树 —> 根结点
层次遍历:只需按层次遍历即可

二叉查找树的缺点

极端情况下会退化为线性链表,二分查找也会退化为遍历查找,时间复杂退化为 O(N),检索性能急剧下降。

平衡二叉查找树缺点

在调整二叉树的形态上消耗的性能会更多。
每一个树节点只存储了一个数据,我们一次磁盘 IO 只能取出来一个节点上的数据加载到内存里,那比如查询 数据就要进行多次磁盘,消耗时间。

满二叉树是什么

除了叶子节点外每一个节点都有两个子节点,且所有叶子节点都在二叉树的同一高度上。

完全二叉树是什么

如果二叉树中除去底层节点后为满二叉树,且底层节点依次从左到右分布。

红黑树是什么,是二叉树的一种吗

红黑树是一种自平衡二叉搜索树(BST),且红黑树节点遵循以下规则:

每个节点只能是红色或黑色
根节点总是黑色的
红色节点的父或子节点都必然是黑色的(两个红色的节点不会相连)
任一节点到其所有后代NULL节点的每条路径都具有相同数量的黑色节点
每个Null节点都是黑色的

HashMap

HashMap底层实现原理

  1. JDK1.7:数组+链表
  2. JDK1.8:数组+链表/红黑树。当链表长度超过8时就转换为红黑树

HashMap扩容机制

当元素个数超过数组长度*负载因子。则把数组大小扩大一倍。
使用rehash方法,节点被分配到“原位置+旧容量”(看原来hash增加的bit位是0还是1,0不变)。

HashMap中初始化大小为啥是16,

为了提升取模的效率,使用位运算代替了取模运算,这就要求Map的容量一定得是2的幂。默认值为16是为了降低hash碰撞

为啥链表长度为8是变成红黑树,为啥6时又变成链表

为了避免频繁来回转化
红黑树占用的内存是链表的来两倍,为了时间和空间,节点的分布频率遵循泊松分布,链表长度达到8个元素的概率很低,

HashMap的遍历方式

for (Map.Entry<String, String> entry : map.entrySet()) {
        System.out.println(entry.getKey() + ":" + entry.getValue());
    }
 for (String key : map.keySet()) {
        System.out.println(key + ":" + map.get(key));
    }
Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
    Map.Entry<String, String> entry = iterator.next();
    System.out.println(entry.getKey() + ":" + entry.getValue());
}
Iterator<String> iterator = map.keySet().iterator();
while (iterator.hasNext()) {
    String key = iterator.next();
    System.out.println(key + ":" + map.get(key));
}
map.forEach((key, value) -> {
    System.out.println(key + ":" + value);
});
map.entrySet().stream().forEach((entry) -> {
    System.out.println(entry.getKey() + ":" + entry.getValue());
});
map.entrySet().stream().parallel().forEach((entry) -> {
        System.out.println(entry.getKey() + ":" + entry.getValue());
    });

hashmap插入过程

  1. 判断存放链表头节点的数组tab是否为空,若为空则扩容,
  2. 否则将数组长度-1与hash值进行位与运算得到一个数组下标,并获取该下标数组元素的值
  3. 判断是否为空,若为空,则创建一个新链表节点放上
  4. 不为空则判断这个元素的hash值与key是否相同
  5. 相同则判断覆盖
  6. 不相同则判断是否为树节点,是树节点则调用树节点插入方法,不是树节点,则用链表插入法

HashTable底层实现。

底层实现:数组+链表。计算key的hash值,二次hash后对应到数组下标,无冲突则存入,有冲突,相同则取代,不同则插入。

HashMap和HashTable的区别

  1. HashMap可接受空键值,而Hashtable则不行。
  2. HashMap是非synchronized,Hashtable是synchronized,是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。
  3. 由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable
  4. HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。
  5. HashMap不能保证随着时间的推移Map中的元素次序是不变的。

linkedhashmap

LinkedHashMap是HashMap的子类,但是内部还有一个双向链表维护键值对的顺序,每个键值对既位于哈希表中,也位于双向链表中。LinkedHashMap支持两种顺序插入顺序 、 访问顺序
1.插入顺序:先添加的在前面,后添加的在后面。修改操作不影响顺序
2.访问顺序:所谓访问指的是get/put操作,对一个键执行get/put操作后,其对应的键值对会移动到链表末尾,所以最末尾的是最近访问的,最开始的是最久没有被访问的,这就是访问顺序。

hashmap linkedhashmap 区别

hashCode()和equals()之间的关系:

hashCode()获取哈希码。对象加入HashSet时,会计算对象hashCode判断加入位置,若有值会调用equals检查是否相同,相同则不加,不同则散列到别的位置。

什么时候用数组快,什么时候用hash快

查询时数组快
插入和删除的时候,用hash快

改进hash算法

链表法和开放地址法

集合

Java集合框架中有哪些

1.集合:List、set、queue
2.图:hashmap,treemap,LinkedHashMap

List、Set、Map三种集合类

  1. List以特定索引来存取元素,可以有重复元素。
  2. Set不能存放重复元素(用对象的equals()方法来区分元素是否重复)。
  3. Map保存键值对(key-value pair)映射,映射关系可以是一对一或多对一。
    Set和Map容器都有基于哈希存储(HashSet/HashMap)和排序树(TreeSet/TreeMap)的两种实现版本,基于哈希存储的版本理论存取时间复杂度为O(1),而基于排序树版本的实现在插入或删除元素时会按照元素或元素的键(key)构成排序树从而达到排序和去重的效果。

Java集合框架中有哪些是基于树实现的?如果这个树不平衡的话怎么办

TreeSet/TreeMap:红黑树
保持平衡:左旋,右旋,着色

数组链表区别

  1. 数组在栈上分配内存,使用方便但是自由度小;链表在堆上分配内存,自由度大
  2. 数组顺序存储,访问效率高;链表访问率低
  3. 数组存在访问越界问题,链表无

堆和栈的区别

堆是什么:

堆、栈和队列有哪些适用场景

List和Set区别:

List:有序,允许重复和多个null元素对象,可使用Iterator取出所有元素,再逐一遍历,还可以使用get(int index)获取指定下标的元素。
Set:无序,不允许重复和多个null元素对象,只能用Iterator取出所有元素,逐一遍历。

ArrayList和LinkedList区别:

ArrayList底层基于数组实现,适合随机查找
LinkedList底层基于链表实现,适合删除、添加、查询,
他们都实现了List接口但LinkedList还实现了Deque接口·,可以当队列使用。

ArrayList与数组之间的转换

String,StringBuffer,StringBuilder

String,StringBuffer,StringBuilder区别:

String:不可变,若改变会生成新的字符串对象。
StringBuilder:可变,线程不安全。比 StringBuffer 快
StringBuffer:可变,线程安全和同步,在单线程下效率更高。

stringbuffer是怎么保证线程安全的

直接通过synchronized 关键字来实现同步操作

String类型的几种存储方式

1.直接赋值创建:存在常量池中
2.new String()创建:(运行时)堆中

String是否线程安全

String存储的字符串都是final,从出生到销毁都不能改变·,所以线程安全。

java面向对象

java和c语言的区别

说一下开发中常用的类

  1. String表示字符串,一旦创建无法改变,改了内容就改了对象和地址
  2. StringBuilder:线程不安全的可变字符序列
  3. Scanner、Math、Random、Date、System

java的三大特性:

封装、继承、多态

多态

同一操作用于不同对象,可以产生不同执行结果
继承+重写

重载重写区别:

重载:一个类中多个同名方法,但是有不同参数个数或参数类型。访问修饰符和返回类型可能不同。
重写:在子类中定义与父类有相同名称和参数,访问权限更高。重写满足自己的方法。

接口和抽象类的区别:

接口中所有方法隐含的都是抽象的,而抽象类可以同时包含抽象和非抽象的方法。一个类只能继承一个抽象类,但可以实现多个接口。类实现接口必须实现接口声明所有方法,但是可以不实现抽象类声明的方法(此时类必须声明为抽象的)。接口是绝对抽象,不能被实例化,抽象类也不可以除非包含main方法可以被调用。
接口:implements变量只能是常量public static final,方法只能是抽象public abstract。没有构造方法。
抽象类:用·abstract表示的。extends变量常量或变量,方法抽象或非抽象。有构造方法,用于子类实例化。

java类加载

将Class文件中类的元信息加载进内存,创建class对象并进行解析,初始化类变量等过程

父类子类构造函数,静态代码段,非静态代码段加载顺序

静态方法加载机制

java创建对象时需要注意哪些方面

所以要么将方法也改为静态方法,要么通过创建该方法所在的实例再通过实例来调用。
是因为内部类是动态的(无static关键字修饰),而main方法是静态的,普通的内部类对象隐含地保存了一个引用,指向创建它的外围类对象,所以要在static方法(类加载时已经初始化)调用内部类的必须先创建外部类。

单例模式中的对象和实例

在单例模式中,每个bean定义只生成一个对象实例。
  单例模式的构造函数是私有的,没有办法直接使用new调用构造函数,所以不会创建新对象。它只能通过它的一个静态方法得到实例,而这个静态方法可以去调构造函数产生一个实例并返回。
  单例模式的作用 :可以保证在程序运行过程,一个类只有一个实例,而且该实例易于供外界访问,从而方便地控制了实例个数,并节约系统资源。
  单例模式的使用场合:在整个应用程序中,共享一份资源(这份资源只需要创建初始化1次),应该让这个类创建出来的对象永远只有一个。

单例模式中对象的创建与销毁

单例模式并不是它的对象永远就只有一个。而是在该类已经创建一个对象的情况下,不允许再创建另一个对象。所以单例模式的对象并不是在服务启动时创建,在服务停止时销毁。它也是在用到的时候创建,不用的时候销毁。
  具体就是:服务启动的时候,创建的是类的字节码,在用到时候类加载器读取字节码,生成一个实例,这样就创建出了一个对象。当长时间不使用对象的时候,对象就会被垃圾器回收。下次创建的时候,依然是读取字节码。

java垃圾回收机制

哪些对象需要回收?

在堆内存中没用被任何栈内存引用的对象

如何判断对象是否存活

  1. 引用计数法:给每个对象加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;计数器为0时即被垃圾回收器回收。(无法解决对象减互相循环引用的问题)
  2. 可达性分析法:以“GC Roots”作为对象的起点,从此节点开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

哪些对象可作为GC Roots?

虚拟机栈(栈帧中的本地变量表)中引用的对象;
方法区中类静态属性引用的对象;
方法区中常量引用的对象;
本地方法栈中JNI(Native方法)引用的对象;
活跃线程的引用对象。

垃圾回收算法

标记清除算法
复制算法
标记整理算法。
分代收集算法。

标记清除算法

复制算法

标记-整理算法

分代收集算法

垃圾回收算法的组合拳。
按照对象生命周期的不同划分区域以 采用不同的垃圾回收算法。
目的: 提高JVM的 回收效率。

一个对象从加载到JVM,再到被GC清除

  1. ⽤户创建⼀个对象,JVM⾸先到⽅法区去找对象的类型信息。然后再创建对象。
  2. JVM要实例化⼀个对象,⾸先要在堆当中先创建⼀个对象。-> 半初始化状态
  3. 对象⾸先会分配在堆内存中新⽣代的Eden。然后经过⼀次Minor GC,对象如果存活,就会进⼊S区。在后续的每次GC中,如果对象⼀直存活,就会在S区来回拷⻉,每移动⼀次,年龄加1。-> 多⼤年龄才会移⼊⽼年代? 年龄最⼤15, 超过⼀定年龄后,对象转⼊⽼年代。
  4. 当⽅法执⾏结束后,栈中的指针会先移除掉。
  5. 堆中的对象,经过Full GC,就会被标记为垃圾,然后被GC线程清理掉

GC分为两类

Minor GC: 用来处理年轻代区域。
Full GC: 用来收集老年代区域。

导致Full GC的几种情况

  1. System.gc()方法的调用
  2. 老年代空间不足
  3. 永生区空间不足
  4. 堆中分配很大的对象

堆的内存划分

年轻代、老年代、永久代

设计一个引用计数垃圾回收算法

引用计数算法在多线程情况下有啥问题

java有垃圾回收机制,为什么还会出现内存泄漏

对象不需要了,还保留了内存和访问方式。

基本数据类型

基本数据类型有哪些,各占多大字节

在这里插入图片描述

基本数据类型为什么还有包装类

int和Integer有什么区别?

Integer是int的包装类,int则是java的一种基本数据类型 Integer变量必须实例化后才能使用,而int变量不需要 Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值 Integer的默认值是null,int的默认值是0

什么时候用包装类,什么时候用基本类型?

  1. 在pojo类中定义的属性用包装类
  2. 在rpc方法中定义参数和返回值的类型用包装类
  3. 定义局部变量用基本类型

final、finally、finalize区别

  1. final用于声明属性,方法和类,分别表示属性不可交变,方法不可覆盖,类不可继承。
  2. finally是异常处理语句结构的一部分,表示总是执行。
  3. finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法供垃圾收集时的其他资源回收,例如关闭文件等。
    Final:关键字,修饰符
    Finally:异常处理机制
    Finalize:一个方法名

static

修饰的成员变量、常量、方法称为静态变量、常量、方法,统称为静态成员,被类的所有实例共享。
在运行时,只分配一次内存,在类内部,可以在任何方法内访问,在其他类,通过类名访问

==和equal的区别

1.类型不同,equals 是方法,而 == 是操作符;
2.比较对象不同:==用于对于基本类型的变量。对于基本类型变量的比较,使用 == 比较, 一般比较的是它们的值。对于引用类型的变量来说(例如 String 类)才有 equals 方法,
3.运行速度:==更快
4.比较内容:
equals先比较地址值后String值,相等即true;
==:比较地址,即是否是同一个对象

java值传递和引用传递

值传递:在调用函数时,将实际参数复制一份传递到函数中,这样修改参数不会影响到原来的参数。
引用传递:将实际参数地址传递,会影响到实际参数。

java内存区域划分

线程共享:堆、方法区
线程独有:栈

深拷贝,浅拷贝

浅复制:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。
深复制:在计算机中开辟一块新的内存地址用于存放复制的对象

查找为O(1)的数据结构

(只知道hash表,数组查找也快)

Java区域

Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。
在这里插入图片描述
在这里插入图片描述

Java内存模型

Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图
在这里插入图片描述
需要注意的是,JMM与Java内存区域的划分是不同的概念层次,更恰当说JMM描述的是一组规则,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式,JMM是围绕原子性,有序性、可见性展开的(稍后会分析)。JMM与Java内存区域唯一相似点,都存在共享数据区域和私有数据区域,在JMM中主内存属于共享数据区域,从某个程度上讲应该包括了堆和方法区,而工作内存数据线程私有数据区域,从某个程度上讲则应该包括程序计数器、虚拟机栈以及本地方法栈。或许在某些地方,我们可能会看见主内存被描述为堆内存,工作内存被称为线程栈,实际上他们表达的都是同一个含义。关于JMM中的主内存和工作内存说明如下

主内存

主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量),当然也包括了共享的类信息、常量、静态变量。由于是共享数据区域,多条线程对同一个变量进行访问可能会发现线程安全问题。

工作内存

主要存储当前方法的所有本地变量信息(工作内存中存储着主内存中的变量副本拷贝),每个线程只能访问自己的工作内存,即线程中的本地变量对其它线程是不可见的,就算是两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,当然也包括了字节码行号指示器、相关Native方法的信息。注意由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题。

弄清楚主内存和工作内存后,接了解一下主内存与工作内存的数据存储类型以及操作方式,根据虚拟机规范,对于一个实例对象中的成员方法而言,如果方法中包含本地变量是基本数据类型(boolean,byte,short,char,int,long,float,double),将直接存储在工作内存的帧栈结构中,但倘若本地变量是引用类型,那么该变量的引用会存储在功能内存的帧栈中,而对象实例将存储在主内存(共享数据区域,堆)中。但对于实例对象的成员变量,不管它是基本数据类型或者包装类型(Integer、Double等)还是引用类型,都会被存储到堆区。至于static变量以及类本身相关信息将会存储在主内存中。需要注意的是,在主内存中的实例对象可以被多线程共享,倘若两个线程同时调用了同一个对象的同一个方法,那么两条线程会将要操作的数据拷贝一份到自己的工作内存中,执行完成操作后才刷新到主内存,简单示意图如下所示:
在这里插入图片描述

观察者使用场景

举报

相关推荐

0 条评论