面试题汇总

文章目录

1.Java的基础知

1.1 java 1.8新特性

在这里插入图片描述

1.2 有哪些集合?


java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。

Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。

1.3 HashMap

《Map到HashMap的一步步实现》
1.7 及之前数组+链表
1.8 数组+(链表+红黑树)

数据比较少的时候,链表的性能要好于红黑树,所以HashMap一上来并没有直接采用红黑树

树化的阈值为8,为什么呢,因为正常业务情况下都不会出现链表的长度超过8。只有在恶意被攻击的情况下,如:DOS攻击,等才会出现大量的链表长度超过的情况,所以使用红黑树用来避免DOS攻击,防止链表超长时性能下降,同时,一般情况下认为树化是偶然情况。

其次,hash值如果足够随机,则在hash表内按泊松分布,在负载因子0.75的情况下,长度超过8的链表出现的概率是0.00000006,选择8的原因就是让树化的几率非常小。

树化的两个条件:链表长度超过树化阈值8,而且数组容量>=64

HashMap 扩容的特点:默认的table 表的大小是 16,threshold 为 12。负载因子 loadFactor .75,这些都是可以构造是更改。以后扩容都是 2 倍的方式增加。

对于为何负载因子是0.75的问题,下面我简单说一下我的感想

首先负载因子的作用是什么?

比如说当前的容器容量是16,负载因子是0.75,16*0.75=12,也就是说,当容量达到了12的时候就会进行扩容操作。

他的作用很简单,相当于是一个扩容机制的阈值。当超过了这个阈值,就会触发扩容机制。

那么为何是0.75呢?

当负载因子为1.0时,意味着只有当HashMap装满之后才会进行扩容,虽然空间利用率有大的提升,但是会导致大量的Hash冲突,使得查询效率变低。

当负载因子为0.5时或者更低的时候,,hash冲突降低,查询效率提高,但由于负载因子太低,导致原来只需要1M的空间存储信息,现在用来2M的空间,最终结果是空间利用率太低。

  • 退化的情况1:在扩容拆分树时,树元素个数小于等于6则会退化为链表退化情况2:remove树节点时,若root、root.left、root.right、root.left.left有一个为空,也会退化为链表

HashMap 从链表到红黑树的转变,如果链表的长度(冲突的节点数)已经达到8个,此时会调用 treeifyBin() ,treeifyBin() 首先判断当前hashMap 的 table的长度,如果不足64,只进行resize,扩容table,如果达到64,那么将冲突的存储结构为红黑树。

treeifyBin() 决定了一个链表何时转化为一个红黑树

链表转化为红黑树的时候并不是直接去转,而是先构建一个标准查询二叉树,然后在标准查询二叉树然后调整为一个红黑树。而balanceInsertion() 实现了调整。

红黑树的性质:

  • 每个结点或是红色的,或是黑色的
  • 根节点是黑色的
  • 每个叶结点(NIL)是黑色的
  • 如果一个节点是红色的,则它的两个儿子都是黑色的。
  • 对于每个结点,从该结点到其叶子结点构成的所有路径上的黑结点个数相同。

红黑树也是一种平衡二叉树,每个节点有一个储存位表示节点的颜色,可以是红色或者黑色。通过对任意一条从根到叶子的路径上各个节点着色方式的限制,红黑树确保没有任意一条从根到叶子的路径超过最短路径的两倍,因此红黑树是一种弱平衡二叉树。相对于AVL树来说,红黑树的旋转次数少,对于搜索、插入、删除多的操作下用红黑树。

1.4 HashMap 是线程安全的吗,会有什么线程安全问题?

HashMap在并发场景中不是线程安全的。

比如A希望插入一个key-value对到HashMap中,当获取到对应的链表结点位置时,此时线程A的时间片用完了,而此时线程B被调度得以执行,可能线程B占用了A计算得到的位置,插入了数值。而线程A被切换回来的时候,不知道B已经插入了元素,仍然将元素插入此前计算好的位置,这样就会将B线程的插入记录覆盖掉了,这对应了多线程的put可能导致元素的丢失;

JDK1.7的时候采用头插法,多线程同时插入的时候,A线程在插入节点B,B线程也在插入,遇到容量不够开始扩容,重新hash,放置元素,采用头插法,后遍历到的B节点放入了头部,这样形成了环。

JDK1.8采用尾插法,会造成两种情况两个线程同时插入只有一个成功插入,还有就是可能会造成两次resize(++size>threshold) 。

如何处理HashMap的线程不安全的问题

使用线程更安全的HashTable或Current类,如:CurrentHashMap,以及使用使用Collections.synchronizedMap方法,对方法进行加同步锁;

1.5 ConcurrentHashmap 用什么方式实现的?Segment 继承了什么类?-- ReentrantLock)

HashTable 是线程安全的。HashTable 容器使用 synchronized 来保证线程安全,但在线程竞争激烈的情况下 HashTable 的效率非常低下.

ConcurrentHashMap 是由 Segment 数组结构和** HashEntry 数组**结构组成。

Segment 是一种可重入锁 ReentrantLock,在 ConcurrentHashMap 里扮演锁的角色

HashEntry 则用于存储键值对数据。一个 ConcurrentHashMap 里包含一个 Segment 数组,Segment 的结构和 HashMap 类似,是一种数组和链表结构, 一个 Segment 里包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素, 每个 Segment 守护者一个 HashEntry 数组里的元素, 当对 HashEntry 数组的数据进行修改时,必须首先获得它对应的 Segment 锁。

使用ConcurrentHashMap ,分段锁的思想,将 HashMap 进行切割,把 HashMap 中的哈希数组切分成小数组,每个小数组有 n 个 HashEntry 组成,其中小数组继承自ReentrantLock(可重入锁),这个小数组名叫Segment(JDK1.7)

JDK1.8中取消了Segment 分段锁,采用 CAS + synchronized 来保证并发安全,ConcurrentHashMap 中 synchronized 只锁定当前链表或红黑二叉树的首节点,只要节点 hash 不冲突,就不会产生并发

那些年你啃过的ConcurrentHashMap

1.6 HashMap 为什么要使用红黑树?

1.7 及之前数组+链表
1.8 数组+(链表+红黑树)

数据比较少的时候,链表的性能要好于红黑树,所以HashMap一上来并没有直接采用红黑树

树化的阈值为8,为什么呢,因为正常业务情况下都不会出现链表的长度超过8。只有在恶意被攻击的情况下,如:DOS攻击,等才会出现大量的链表长度超过的情况,所以使用红黑树用来避免DOS攻击,防止链表超长时性能下降,同时,一般情况下认为树化是偶然情况。

其次,hash值如果足够随机,则在hash表内按泊松分布,在负载因子0.75的情况下,长度超过8的链表出现的概率是0.00000006,选择8的原因就是让树化的几率非常小。

树化的两个条件:链表长度超过树化阈值8,而且数组容量>=64

  • 退化的情况1:在扩容拆分树时,树元素个数小于等于6则会退化为链表退化情况2:remove树节点时,若root、root.left、root.right、root.left.left有一个为空,也会退化为链表

HashMap 从链表到红黑树的转变,如果链表的长度(冲突的节点数)已经达到8个,此时会调用 treeifyBin() ,treeifyBin() 首先判断当前hashMap 的 table的长度,如果不足64,只进行resize,扩容table,如果达到64,那么将冲突的存储结构为红黑树。

treeifyBin() 决定了一个链表何时转化为一个红黑树

链表转化为红黑树的时候并不是直接去转,而是先构建一个标准查询二叉树,然后在标准查询二叉树然后调整为一个红黑树。而balanceInsertion() 实现了调整。

红黑树的性质:

  • 每个结点或是红色的,或是黑色的
  • 根节点是黑色的
  • 每个叶结点(NIL)是黑色的
  • 如果一个节点是红色的,则它的两个儿子都是黑色的。
  • 对于每个结点,从该结点到其叶子结点构成的所有路径上的黑结点个数相同。

红黑树也是一种平衡二叉树,每个节点有一个储存位表示节点的颜色,可以是红色或者黑色。通过对任意一条从根到叶子的路径上各个节点着色方式的限制,红黑树确保没有任意一条从根到叶子的路径超过最短路径的两倍,因此红黑树是一种弱平衡二叉树。相对于AVL树来说,红黑树的旋转次数少,对于搜索、插入、删除多的操作下用红黑树。

1.7 ArrayList 扩容的函数方法名是什么?

底层实现:ArrayList底层实现是基于动态数组,对内存要求比较高,需要一块连续的内存空间
LinkedList底层使用的是双向链表数据结构,不需要连续的内存空间,元素分散存储在内存里

效率:ArrayList增删慢,查询快,对元素必须连续存储,查询复杂度o(1),新增复杂度o(n^2)
LinkedList,查询复杂度o(n),插入复杂度o(n)

默认容量和扩容机制:ArrayList,默认容量是10,如果初始化时一开始指定了容量,或者通过集合作为元素,则容量为指定的大小或参数集合的大小,扩容机制是新增到原来的1.5倍数,如果新增后超过这个容量,则容量为新增后所需的最小容量,然后使用Arrays.copy这样一个方法把老数组的数据拷贝的新的数组里面,扩容完成以后,再把当前需要插入的元素插入到新数组当中。

扩容的主要方法是grow(),我的理解是扩容最小应该满足的容量(因为ArrayList的扩容并不是每次扩容一位的,所以这个是最小的)

1.8. final、finally、finallize?

final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。

finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。

finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调用System的gc()方法的时候,由垃圾回收器调用finalize(),回收垃圾。

1.9 finally 是在 return 之前执行还是之后?finally 块里的代码一定会执行吗?

try-catch-finally 中,如果 catch 中 return 了,finally也会执行,只不过是在return之前执行
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

1.10 lambda 表达式了解吗?使用 lambda 表达式怎么实现两个 list 的 交集(提示:filter)?

lambda表达式本质是为了解决匿名内部类的问题,使代码变得简洁。

Lambda表达式是JAVA 8中提供的一种新的特性,基于函数式编程的思想,支持以代码作为方法参数,Lambda表达式本身没有名称,也不需要强制其归属于某个类,同时它又能有参数列表,方法体,返回值等,可以想象成是一种表达方式更简洁的匿名函数,所以可以编写Lambda表达式来优化匿名内部类的使用,提高代码的可读性。

lambda表达式取交并补

  // 交集
        List<Student> interp = list1.stream().filter(list2Set::contains).collect(toList());
        System.out.println("---得到交集 interp---");
        interp.parallelStream().forEach(System.out::println);

        // 差集 (list1 - list2)
        List<Student> reduce1 = list1.stream().filter(item -> !list2Set.contains(item)).collect(toList());
        System.out.println("---得到差集 reduce1 (list1 - list2)---");
        reduce1.parallelStream().forEach(System.out::println);

        // 差集 (list2 - list1)
        List<Student> reduce2 = list2.stream().filter(item -> !list1Set.contains(item)).collect(toList());
        System.out.println("---得到差集 reduce2 (list2 - list1)---");
        reduce2.parallelStream().forEach(System.out::println);

        // 并集
        List<Student> listAll = list1.parallelStream().collect(toList());
        List<Student> listAll2 = list2.parallelStream().collect(toList());
        listAll.addAll(listAll2);
        System.out.println("---得到并集 listAll---");
        listAll.parallelStream().forEach(System.out::println);

        // 去重并集
        list1Set.addAll(list2Set);
        List<Student> listDistinctAll = new ArrayList<>(list1Set);
        System.out.println("---得到去重并集 listDistinctAll---");
        listDistinctAll.parallelStream().forEach(System.out::println);

1.11 深拷贝和浅拷贝

深拷贝和浅拷贝就是指对象的拷贝,一个对象中存在两种类型的属性,一种是基本數据类型, 一种是实例对象的引用。

1.浅拷贝是指,只会拷贝基本数据类型的值,以及实例对象的引用地址,并不会复制一份引用地址所指向的对象, 也就是浅拷贝出来的对象,内部的类属性指向的是同一个对象

2深拷贝是指,既会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,内部的类执行指向的不是同一个对象

1.12 Java 序列化和反序列化为什么要实现 Serializable 接口?

https://blog.youkuaiyun.com/zhiyikeji/article/details/126547155?spm=1001.2014.3001.5501

1.13 private修饰的方法可以通过反射访问,那么private的意义是什么?

https://blog.youkuaiyun.com/zhiyikeji/article/details/125922970?spm=1001.2014.3001.5501

1.14 带你彻底认识String

https://blog.youkuaiyun.com/zhiyikeji/article/details/125883773?spm=1001.2014.3001.5501

1.15 谈谈你对Map的理解

https://blog.youkuaiyun.com/zhiyikeji/article/details/124717489?spm=1001.2014.3001.5501

1.16 isEmpty 和 isBlank 的用法区别

https://blog.youkuaiyun.com/zhiyikeji/article/details/123200038?spm=1001.2014.3001.5501

1.17 类与对象的关系是什么

类和对象之间是抽象与具体的关系。类是一个模板,是对一类事物的抽象描述,而对象用于表示现实中该事物的个体。类是在对象之上的抽象,对象则是类的具体化,是类的实例。

类必须通过对象才可以使用,而对象中的属性和行为必须在类中定义。

什么是对象?

对象(Object)是一个应用系统中的用来描述客观事物的实体,是有特定属性和行为(方法)的基本运行单位。是类的一个特殊状态下的实例。对象可以是一个实体、一个名词、一个可以想象为有自己标识的任何东西,可以概括来说:万物皆对象。

什么是类?

类(Class)是Java代码的基本组织模块,使用来描述一组具有共同属性和行为对象的基本原型。是对这组对象的概括、归纳和描述表达。类是对象的模板,它定义了本类对象的所拥有的属性集和行为集,是对一组具有相同属性和相同方法的对象的定义。

1.18 Java 序列化和反序列化为什么要实现 Serializable 接口?

https://blog.youkuaiyun.com/zhiyikeji/article/details/126547155?spm=1001.2014.3001.5501

1.19 SimpleDateFormat是线程安全的吗

SimpleDateFormat不是线程安全的,因为他的内部维护了一个Calendar的一个对象引用,他是用来存储和这个SimpleDateFormat相关的日期信息,当我们把SimpleDateFormat作为多个线程共享资源来使用的时候,意味着多个线程会共享SimpleDateFormat里面的一个Calendar引用,那么多个线程对同一个Calendar对象的一个操作,就会出现数据脏读导致一些不可预料的一些错误,在实际应用过程中呢,我认为 有四种方法可以解决这个问题:

1.把SimpleDateFormat定义成一个局部变量,每个线程调用这个方法的时候都会创建一个新的实例;

2.我们可以使用ThreadLocal这样一个工具,把SimpleDateFormat变成一个线程私有的

3.加一个同步锁,在同一个时刻只允许一个线程去操作,如:Syschronized关键字。

4.在Java8里面呢引入了一些线程安全的日期API,比如说像LocalDateTimer,比如说像LocalDateTimerFormatter等

1.20 简述一下受检异常和非受检异常

对于这个问题可以通过两个方面回答:

第一个是异常的本质

受检异常和非受检异常都是派生自Throwable,他有两个子类的实现:

  • error
  • Exception

error是指程序无法处理的错误,他和程序本身没有关系

而exception是指程序运行的时候他需要处理的异常

受检异常和非受检异常都是派生自exception类

第二个是对于受检异常和非受检异常的定义

受检异常是指在编译阶段必须要主动捕获的异常,遇到该异常他有两种处理办法:

  • 通过try catch捕获
  • 或者通过throw把异常抛出

非受检异常是指程序不需要主动捕获,一般发生在程序运行期间,比如:Nullpointexception,当然我们可以选择去主动捕获这些异常。

出现这些异常的时候虽然说我们无法通过 捕获异常来实现异常的恢复,但是这些异常信息可以帮助我们快速去定位问题。

1.21 并行和并发有什么区别

  • 并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
  • 并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
  • 在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群。所以并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。

1.22 Object类的子类

  • registerNatives() //私有方法
  • getClass() //返回此 Object 的运行类。
  • hashCode() //用于获取对象的哈希值。
  • equals(Object obj) //用于确认两个对象是否“相同”。
  • clone() //创建并返回此对象的一个副本。
  • toString() //返回该对象的字符串表示。
  • notify() //唤醒在此对象监视器上等待的单个线程。
  • notifyAll() //唤醒在此对象监视器上等待的所有线程。
  • wait(long timeout) //在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量前,导致当前线程等待。
  • wait() //用于让当前线程失去操作权限,当前线程进入等待序列
  • finalize() //当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。

1.23 JDK中常用的设计模式

1.单例模式

  • 作用:保证一个类只有一个实例
  • JDK中的体现:Runtime类

2.静态工厂模式

  • 作用:代替构造函数创建对象,方法名比构造函数清晰
  • JDK中的体现:包装类。比如:Intefer.valueOf(),Class.forName()等

3.抽象工厂模式

  • 作用:创建某一种类的对象
  • JDK中的体现:Java.sql包

4.原型模式

  • 作用:原型模式的本质是拷贝原型来创建新的对象,拷贝是比new更快的创建对象的方法,当需要大批量创建新对象,而且都是同一个类的对象的时候可以考虑原型模式
  • JDK中的体现:clone()方法

5.适配器模式

  • 作用:是不兼容的接口相兼容。
  • JDK中的体现:InputStream、OutputStrem

6.装饰器模式

1.24 公共类的作用

“这个主要是保护属性的作用,防止在其他类对它进行改变。
我们可以想象,如果定义成public的话,那么在任何地方都可以进行访问,那么是相当危险的。有些变量我们需要让外界访问的话,可以提供一个get方法。还有属性本来就是一个类私有的东西,定义成private没有什么错误。

1.25 怎么调用总部接口

Java:

  @Autowired
    private RestOperations restTemplate;
     MultiValueMap<String, Object> bodyMap = new LinkedMultiValueMap<>();
      bodyMap.add("file", new FileSystemResource(image));
        bodyMap.add("fileName", null);
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.MULTIPART_FORM_DATA);
        /**此处的请求头设置是必要的,您只有在注册之后才能够使用我们的文件上传服务**/
        headers.add("X-Token", token);
        HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(bodyMap, headers);
        RestTemplate restTemplate = new RestTemplate();
        ResponseEntity<BaseResult> res = restTemplate.exchange("远程ip+端口地址",
                HttpMethod.POST, 
                requestEntity, BaseResult.class);
        String reImgUrl = res.getBody().getData().toString();//返回的在线地址
        return res.getBody();

要记得将restTemplate添加到Bean中:

@Configuration
public class ConfigBean {
   
    //@Configuration 相当于 spring中的 application.xml

    @Bean
    RestTemplate restTemplate(){
   
   
        return new RestTemplate();
    }
}

ajax:

var ip = "ip+端口号";
            $.ajax({
   
   
                url: ip,
                data: formData,
                dataType: "json",
                type: "post",
                success: function(data) {
   
   
                    console.log(data)
                }
            });

axios:

          const formData = new FormData();
            formData.append("file", file);
            formData.append("fileName", "fileName"); //注意可以没有此项
            var ip = "ip+端口号";
     axios({
   
   
            method: "POST",
            url: ip,
            data: formData, //参数
            headers: {
   
    "X-Token": "您的token" },
          })
            .then((resp) => {
   
   
              console.log(resp)
            })
            .catch((err) => {
   
   
              console.log(err)
            });

python:

  import requests

  headers2 = {
   
   'Content-Type': 'application/json'}
  data = {
   
   "name": "ninesun", "age": 21}
  r = requests.post('http://localhost:3010/v1/carstatistic-3003/carInfo/countCarInfo', headers=headers2,json=data,verify=False)
  r.headers
  print(r.text)

1.26 equals == 区别

equals方法对于字符串来说是比较内容的,而对于非字符串来说是比较其指向的对象是否相同的。

== 比较符也是比较指向的对象是否相同的也就是对象在对内存中的的首地址。

String类中重新定义了equals这个方法,而且比较的是值,而不是地址。所以是true。

对于基本类型的包装类型,比如Boolean、Character、Byte、Shot、Integer、Long、Float、Double等的引用变量,==是比较地址的,而equals是比较内容的。

1.27 Java中线程不安全的原因

首先什么是线程安全,线程安全就是多个线程修改同一个变量的时候,修改的结果需要和单线程修改的结果相同。如果修改的结果和预期不符,那就是线程不安全。导致线程不安全的原因有:

1.抢占式执行,这取决于cpu的调度,我们没有权力去改变。

2.多个线程修改同一个变量,注意,这里是同一个变量,通过上面这个图我们看的出来,当线程1在读一个数据的时候,线程2也可能读了原本的数据。在线程1保存之后,线程2还是修改的原来读的值,再进行保存,然后就会覆盖掉线程1修改的值,所以我们本来期望a这个变量是自增两次,但是到最后只自增了一次,与预期结果不符。多线程修改同一个变量这是我们可以改变的,只要我们不修改同一个变量,那每个线程自己干自己的事情,就不会导致线程不安全。

3.原子性,原子性就是这个操作不可以进行拆分,要么全部都执行,要么全部不执行,不能在被执行一半的时候被中断。上面举的例子a++这个操作就是可以拆分的操作,首先需要先读取a的值,再进行++操作,然后给a赋值,所以a++这个操作不是原子性操作,但是如果我们只是读一个或者单纯的赋值操作那就是原子性操作。上面的图就是因为a++这个操作不是原子性,然后线程间抢占式执行带来的结果。所以但是我们保证了操作的原子性,还是会发生线程不安全

4.内存可见性,是指一个线程的操作对于另一个线程来说是可以见的,称为内存可见性,就像上面的图,线程1进行a++这个操作,然后线程2在线程1进行a++之后,读的值并不是a++之后的值,而是原本的值,线程1 a++这个操作对于线程2是不可见的,所以他们不存在内存可见性。

5.指令重排序,指令重排序是编译器自己优化的结果,我们写的代码执行的顺序,可能不是书写的顺序,编译器会自动优化这个代码的顺序,可能会导致线程不安全。

1.28 包装类和基本数据类型区别?为什么有包装类?

在Java中,new的对象存储在堆里,通过栈中的引用来使用这些对象;但是对经常用到的一系列类型如int,如果我们用new将其存储在堆里就不是很有效——特别是简单的小的变量。所以就出现了基本类型,同C++一样,Java采用了相似的做法,对于这些类型不是用new关键字来创建,而是直接将变量的值存储在栈中,因此更加高效。

有了基本类型为啥还要有包装类型?

Java是一个面向对象的编程语言,基本类型并不具有对象的性质,为了让基本类型也具有对象的特征,就出现了包装类型(如我们在使用集合类型Collection时就一定要使用包装类型而非基本类型),它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。

另外,当需要往ArrayList,HashMap中放东西时,像int,double这种基本类型是放不进去的,因为容器都是装object的,这是就需要这些基本类型的包装器类了。

二者的区别:

  1. 声明方式不同:
    基本类型不使用new关键字,而包装类型需要使用new关键字来在堆中分配存储空间;
  2. 存储方式及位置不同:
    基本类型是直接将变量值存储在栈中,而包装类型是将对象放在堆中,然后通过引用来使用;
  3. 初始值不同:
    基本类型的初始值如int为0,boolean为false,而包装类型的初始值为null;
  4. 使用方式不同:
    基本类型直接赋值直接使用就好,而包装类型在集合如Collection、Map时会使用到。

补充:
1、int 不能赋值为null(默认值为0),包装类型Integer可以为null(默认值为null)
2、当int中的数值 与 integer 中的数值一致的时候, 使用 “==” 进行比较,结果为true
3、如果两个都是new的Integer相比较也是相等

1.29 IO体系

体系分为3大部分:流式部分(核心),非流式部分,其他

流式部分:字节流以及字符流

字节流:InputStream,OutputStream

字符流:Reader,Writer

非流式部分:File,RandomAccessFile(随机文件操作类)

IO流的分类

根据处理的数据类型不同可以分为:字符流和字节流

根绝数据流向不同分为:输入流和输出流

按照数据来源(去向)分类:

  • File(文件):FileInputStream,FileOutputStream,FileReader,FileWriter
  • byte[]:ByteArrayInputStream,ByteArrayOutputStream
  • Char[]:CharArrayReader,CharArrayWriter
  • String:StringBufferInputStream,StringReader,StringWriter
  • 网络流数据:InputStream,OutputStream,Reader,Writer

2.Spring篇

简单谈谈你对Spring的理解

Spring就是一个生态,可以构建Java的一切基础设施,通常Spring就是指Spring Framework,他是一个轻量级的开源容器框架,在操作dao层的时候,每次都需要实例化一次,这样是不是就很繁琐;有的人可能使用单例模式来解决这个问题,但业务代码与单例模式的模板代码放在一个类里而且也会出现大量重复的单例模式的模板代码,耦合性较高,要知道java语言可是高内聚,低耦合的;所以伟大的Spring就出现了,也就是类似于数据库连接池的东西。

理解Sping

传统Java SE程序设计,我们直接在对象内部通过new进行创建对象或者getInstance等直接或者间接调用构造方法创建一个对象;

而在Spring开发模式中,Spring容器使用了工厂模式为我们创建了所需要的对象(这个过程就是DI通过setter方法在配置中注入对象),我们使用时不需要自己去创建,直接调用Spring为我们提供的对象即可,这就是控制反转的思想。

实例化一个java对象有三种方式:

  • 使用类构造器
  • 使用静态工厂方法
  • 使用实例工厂方法

当使用spring时我们就不需要关心通过何种方式实例化一个对象,spring通过控制反转机制自动为我们实例化一个对象。

面向切面AOP

在面向对象编程(OOP)思想中,我们将事物纵向抽象成一个个的对象。而在面向切面编程中,我们将一个个对象某些类似的方面横向抽象成一个切面,对这个切面进行一些如权限验证,事物管理,记录日志等公用操作处理的过程就是面向切面编程的思想

Spring支持的几种bean的作用域

Spring框架支持以下五种bean的作用域:

  • singleton:bean在每个Springioc容器中只有一个实例。
  • prototype:一个bean的定义可以有多个实例。
  • request:每次http请求都会创建一个bean,该作用域仅在基于web的SpringApplicationContext情形下有效。
  • session:在一个HTTPSession中,一个bean定义对应一个实例。该作用域仅在基于web的SpringApplicationContext情形下有效。
  • global-session:在一个全局的HTTPSession中,一个bean定义对应一个实例。该作用域仅在基于web的SpringApplicationContext情形下有

使用Spring框架的好处

  • 轻量:Spring是轻量的,基本的版本大约2MB。
  • 控制反转:Spring通过控制反转实现了松散耦合,对象们给出它们的依赖,而不是创建或查找依赖的对象们。
  • 面向切面的编程(AOP):Spring支持面向切面的编程,并且把应用业务逻辑和系统服务分开。
  • 容器:Spring包含并管理应用中对象的生命周期和配置。
  • MVC框架:Spring的WEB框架是个精心设计的框架,是Web框架的一个很好的替代品。
  • 事务管理:Spring提供一个持续的事务管理接口,可以扩展到上至本地事务下至全局事务(JTA)。
  • 异常处理:Spring提供方便的API把具体技术相关的异常(比如由JDBC,HibernateorJDO抛出的)转化为一致的unchecked异常。

2.1 Spring事务

spring 所有的事务管理策略类都继承自

org.springframework.transaction.PlatformTransactionManager 接口

public interface PlatformTransactionManager {
   
   
  TransactionStatus getTransaction(TransactionDefinition definition)
  throws TransactionException;
  void commit(TransactionStatus status) throws TransactionException;
  void rollback(TransactionStatus status) throws TransactionException;
}

Spring 事务实现方式有哪些?

事务就是一系列的操作原子执行。Spring事务机制主要包括声明式事务和编程式事务。

  • 编程式事务:通过编程的方式管理事务,这种方式带来了很大的灵活性,但很难维护。
  • 声明式事务:将事务管理代码从业务方法中分离出来,通过aop进行封装。

我重点说一下声明式事务:

spring 声明式事务,即 @Transactional ,它可以帮助我们把事务开启、提交或者回滚的操作,通过Aop的方式进行管理。

Spring声明式事务使得我们无需要去处理获得连接、关闭连接、事务提交和回滚等这些操作。使用 @Transactional 注解开启声明式事务。
在这里插入图片描述

在 spring 的bean 的初始化过程中,就需要对实例化的bean 进行代理,并且生成代理对象。生成代理对象的代理逻辑中,进行方法调用时,需要先获取切面逻辑,@Transactional 注解的切面逻辑类似于@Around,在spring 中是实现一种类似代理逻辑
在这里插入图片描述

@Transactional 实现原理

@Transactional 实质是使用了JDBC 的事务来进行事务控制的

1.事务开始时,通过AOP机制,生成一个代理connection对象,并将其放入 DataSource 实例的某个与 DataSourceTransactionManager 相关的某处容器中。

在接下来的整个事务中,客户代码都应该使用该 connection 连接数据库,执行所有数据库命令。
不使用该 connection 连接数据库执行的数据库命令,在本事务回滚的时候得不到回滚(物理连接connection逻辑上新建一个会话session;DataSource 与 TransactionManager 配置相同的数据源)

2.事务结束时,回滚在第1步骤中得到的代理connection对象上执行的数据库命令,然后关闭该代理connection对象。

值得我们注意的是:事务结束后,回滚操作不会对已执行完毕的SQL操作命令起作用

spring声明式事务哪些场景会失效

在这里插入图片描述

https://blog.youkuaiyun.com/zhiyikeji/article/details/126675222

事务的隔离级别

事务的隔离级别:是指若干个并发的事务之间的隔离程度

  • @Transactional(isolation = Isolation.READ_UNCOMMITTED):读取未提交数据(会出现脏读, 不可重复读) 基本不使用
  • @Transactional(isolation = Isolation.READ_COMMITTED):读取已提交数据(会出现不可重复读和幻读)
  • @Transactional(isolation = Isolation.REPEATABLE_READ):可重复读(会出现幻读)
  • @Transactional(isolation = Isolation.SERIALIZABLE):串行化

很明显事务的隔离级别和数据库的隔离级别相同

有哪些事务传播行为?

在TransactionDefinition接口中定义了七个事务传播行为:

1. TransactionDefinition.PROPAGATION_REQUIRED:
   如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。
 
2. TransactionDefinition.PROPAGATION_REQUIRES_NEW:
   创建一个新的事务,如果当前存在事务,则把当前事务挂起。
 
3. TransactionDefinition.PROPAGATION_SUPPORTS:
   如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
 
4. TransactionDefinition.PROPAGATION_NOT_SUPPORTED:
   以非事务方式运行,如果当前存在事务,则把当前事务挂起。
 
5. TransactionDefinition.PROPAGATION_NEVER:
   以非事务方式运行,如果当前存在事务,则抛出异常。
 
6. TransactionDefinition.PROPAGATION_MANDATORY:
   如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
 
7. TransactionDefinition.PROPAGATION_NESTED:
   如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;
   如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED

2.2 Spring 用到了哪些设计模式?

1、简单工厂模式:BeanFactory就是简单工厂模式的体现,根据传入一个唯一标识来获得 Bean 对象。

@Override
public Object getBean(String name) throws BeansException {
   
   
    assertBeanFactoryActive();
    return getBeanFactory().getBean(name);
}

2.工厂方法模式:FactoryBean就是典型的工厂方法模式。spring在使用getBean()调用获得该Bean时,会自动调用该Bean的getObject()方法。每个 Bean 都会对应一个 FactoryBean,如 SqlSessionFactory 对应 SqlSessionFactoryBean。

3、单例模式:一个类仅有一个实例。Spring 创建 Bean 实例默认是单例的。

4、适配器模式:SpringMVC中的适配器HandlerAdatper。由于应用会有多个Controller实现,如果需要直接调用Controller方法,那么需要先判断是由哪一个Controller处理请求,然后调用相应的方法。当增加新的 Controller,需要修改原来的逻辑,违反了开闭原则(对修改关闭,对扩展开放)。

为此,Spring提供了一个适配器接口,每一种 Controller 对应一种 HandlerAdapter 实现类,当请求过来,SpringMVC会调用getHandler()获取相应的Controller,然后获取该Controller对应的 HandlerAdapter,最后调用HandlerAdapter的handle()方法处理请求,实际上调用的是Controller的handleRequest()。每次添加新的 Controller 时,只需要增加一个适配器类就可以,无需修改原有的逻辑。

5、代理模式:spring 的 aop 使用了动态代理,有两种方式JdkDynamicAopProxy和Cglib2AopProxy。

6、模板模式:Spring 中 jdbcTemplate、hibernateTemplate 等使用了模板模式。

2.3 IOC与AOP

谈谈你对IOC的理解

IO可以从三个方面去回答:

  • 容器概念
  • 控制反转
  • 依赖注入
IOC容器

IOC容器其实就是一个Map(key,value),里面存的是各种对象(如:在xml中配置的各种bean,@Repository,@Service,@Controller,@Component),在项目启动的时候会读取配置文件里的bean节点,根据全限定类名使用反射创建对象到Map,扫描到打上上述注解的类也同样是通过反射创建对象到Map。

这个时候Map里就有各种对象了,接下来我们在代码里需要用到里面的对象时,在通过DI注入(依赖注入),如常见的注解有:@Autowired、@Resource,以及xml中bean结点的内的ref属性根据id注入。

控制反转

没有引入IOC容器之前,对象A在初始化或者运行到某一点的时候,自己必须主动去创建对象B或者使用已经创建的对象B,无论是使用还是创建对象B,控制权都在A手上。

引入IOC容器之后,对象A与对象B之间就失去了直接联系,当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到A需要的地方。

通过前后的对比不难看出:对象A获取依赖对象B的过程由主动行为变为被动行为,控制权颠倒过来了,这就是控制反转名词的由来。

所有对象的控制权全部上交给第三方IOC容器,所以IOC容器成了整个系统的关键核心,它起到了一种类似粘合剂的作用,把系统中所有的对象粘合在一起发挥作用,如果没有这个粘合剂,对象和对象之间彼此会失去联系,这就是有人把IOC容器比喻成粘合剂的由来。

依赖注入

获得依赖对象的过程被反转了。控制反转之后,获得依赖对象的过程由自身管理变为了由IOC容器主动注入,依赖注入是实现IOC的方法,就是由IOC容器在运行期间,动态的将某种依赖关系注入到对象中。

简述Spring Aop原理

Spring AOP大致分为四个阶段:

  • 创建代理对象
  • 拦截目标对象
  • 调用代理对象阶段
  • 调用目标对象阶段
创建代理对象阶段

在Spring中创建Bean的实例都是从getBean方法开始,在实例创建之后Spring容器会根据AOP的配置去匹配目标类的类名,看目标类是否满足切面规则,如果满足切面规则,就会调用ProxyFactory创建Bean,并且缓存到IOC容器中,然后根据目标对象自动选择不同的代理策略:

  • 如果目标类实现了接口,Spring会默认使用JDK Proxy
  • 如果目标类没有实现接口,Spring会默认选择Cglib Proxy

当然我们也可以通过配置去强制Spring使用Cglib Proxy

拦截目标对象阶段

当用户调用目标对象的某个方法的时候,就会被一个叫做AopProxy的对象拦截,那么Spring将所有的调用策略封装到了这个对象中,它默认实现了一个叫做InvocationHandler的接口,也就是调用代理对象的外层拦截器,在这个接口的invoke()方法中,会触发MethodInvaocation的proceed()方法,在proceed()方法中,会按照顺序执行符合AOP规则的拦截器链。

调用代理对象阶段

Spring Aop拦截链中每个元素都会被命名为MethodInterceptor,也就是切面中的Advice通知,这个通知是可以用来回调的,简单的理解就是生成的代理Bean方法,也就是我们常说的被织入的代码片段,这些被织入的代码片段会在该阶段被执行。

调用目标对象阶段

MethodInterceptor接口也有一个invoke()方法,那么在MethodInterceptor的invoke()方法中,会触发对目标对象的调用,也就是去反射调用目标对象的方法。

下面说几个重要的名词:

  • 代理对象:就是由Spring代理策略生成 的对象
  • 目标对象:就是我们自己写的业务代码
  • 织入代码:在我们自己写得业务代码中增加的代码片段
  • 切面通知:封装织入代码片段的回调方法
  • MethodInvocation:负责执行拦截器链,在proceed()方法中去执行
  • MethodInterceptor:负责执行织入片段的代码,在Invoke()方法中去执行

2.4 Spring中的Bean是线程安全的吗?

先说一下答案:

其实Spring中的Bean是否线程安全跟Spring本身无关,Spring框架中会提供很多线程安全的的策略,因此Spring容器中的Bean本身也不具备线程安全的特性。

要彻底理解上面这个结论,我们首先要知道Spring中的Bean是从哪里来的,在Spring容器中,除了很多Spring内置的Bean以外,其实其它Bean都是我们自己通过Spring配置来声明的,然后由Spring容器来进行统一的管理和加载

我们在Spring声明配置中通常会配置以下内容:

  • class(全类名)
  • id (Bean的唯一标识)
  • scope(作用域)
  • lazy-init (是否延时加载)

之后呢,Spring容器会根据这些配置的内容,使用对应的策略来进行创建实例,因此Spring容器中的Bean,其实都是根据我们自己写的类来创建的实例。

Spring中的Bean是否线程安全,跟Spring容器无关,只是交给Spring容器托管而已,那么在Spring容器中什么样的Bean会存在线程安全问题呢?

在解答这个问题之前,我们得先回顾一下Spring Bean的作用域。

  • prototype(多例Bean)
    每次getBean的时候都会创建一个新的对象
  • singleton(单例Bean)
    在Spring容器中只会存在一个全局共享的实例

根据作用域的定义,多例Bean每次都会创建新的实例,也就是说线程之间不存在Bean共享的问题,因此多例Bean是不存在线程安全问题的。

而单例Bean是所有线程共享一个实例,因此可能会存在线程安全问题。但是呢,单例Bean又分为:

  • 无状态Bean
    在多线程操作中只会对Bean的成员变量进行查询操作,不会修改成员变量的值,这样的Bean成为无状态Bean,所以无状态的单例Bean不存在线程安全问题
  • 有状态Bean
    多线程操作中,如果需要对Bean中的成员变量进行数据更新操作,这种Bean成为有状态Bean,而有状态Bean可能存在线程安全问题

在Spring中,只有有状态的单例Bean才会存在线程安全问题,我们在使用Spring的过程中经常会使用到有状态的单例Bean,如果我们经常遇到线程安全问题,我们又该如何处理呢?

  • 1.修改Bean的作用域,将"singleton"改为"prototype"
  • 2.避免定义可变的成员变量
  • 3.在类中定义ThreadLocal的成员变量,并将需要的可变成员变量保存在ThreadLocal中,因为ThreadLocal本身就具备线程隔离的特点,这就相当于为每个线程提供了一个独立的变量副本,每个线程呢只需要操作自己的线程变量副本,从而解决线程安全的问题。

2.5 谈谈你对Spring Bean的理解

回答这个问题,我们可以从三个方面来回答:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZNineSun

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值