武汉超星学习通Java实习生一面

自我介绍

Java有哪些数据类型

Java数据类型包括基本数据类型和引用数据类型两大类。

基本数据类型有8个,可以分为4个小类,分别是整数类型(byte/short/int/long)、浮点类型(float/double)、字符类型(char)、布尔类型(boolean)。其中,4个整数类型中,int类型最为常用。2个浮点类型中,double最为常用。另外,在这8个基本类型当中,除了布尔类型之外的其他7个类型,都可以看做是数字类型,它们相互之间可以进行类型转换。

引用类型就是对一个对象的引用,根据引用对象类型的不同,可以将引用类型分为3类,即数组、类、接口类型。引用类型本质上就是通过指针,指向堆中对象所持有的内存空间,只是Java语言不再沿用指针这个说法而已。

  • byte
  • short
  • int
  • long
  • float
  • double
  • char
  • boolean
  • 数组
  • 接口

Java中既然有了基本数据类型为什么还要包装类

我们知道Java是一个面向对象的编程语言,基本类型不具有对象的性质,为了让基本类型也具有对象的特征,就出现了包装类型(如我们在使用集合类型Collection时就一定要使用包装类型而非基本类型),
它相当于将基本类型"包装起来",使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。另外,当需要往ArrayList,HashMap中放东西时,像int,double这种基本类型是放不进去的,因为容器都是装object的,这是就需要这些基本类型的包装器类了。

自动装箱、自动拆箱的应用场景

自动装箱、自动拆箱是JDK1.5提供的功能。
自动装箱:可以把一个基本类型的数据直接赋值给对应的包装类型;
自动拆箱:可以把一个包装类型的对象直接赋值给对应的基本类型;
通过自动装箱、自动拆箱功能,可以大大简化基本类型变量和包装类对象之间的转换过程。比如,某个方法的参数类型为包装类型,调用时我们所持有的数据却是基本类型的值,则可以不做任何特殊的处理,直接将这个基本类型的值传入给方法即可。
二者相互转换:(JDK1.5 之后提供了自动拆装箱)

  • int转Integer
    int i = 0;
  • Integer ii = new Integer(i);
    Integer转int
  • Integer ii = new Integer(0);
    int i = ii.intValue();
    二者的区别:
  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相比较也是相等的

static和final关键字的区别

  • static关键字
    static关键字用于修饰类的成员变量、方法和代码块等,它的作用是使这些成员属于类,而不是属于对象。这意味着,不管类实例化多少对象,这些成员都只有一份拷贝,它们被所有类的实例公用。
    静态成员可以通过类名直接访问,而不需要创建类的实例。而非静态成员只能通过创建类的实例才能访问。
  • final关键字
    final关键字用于修饰变量、方法和类等,它的作用是表示这个变量只能被赋值一次,或者这个方法不能被重写,或者这个类不能被继承。
    被final关键字修饰的变量必须在声明时或初始化时进行赋值,之后不能再进行赋值。被final关键字修饰的方法不能被子类重写,被final关键字修饰的类不能被继承。

static关键字可以修饰成员变量、成员方法、初始化块、内部类,被static修饰的成员是类的成员,它属于类、不属于单个对象。以下是static修饰这4种成员时表现出的特征:

  • 类变量:被static修饰的成员变量叫类变量(静态变量)。类变量属于类,它随类的信息存储在方法区,并不随对象存储在堆中,类变量可以通过类名来访问,也可以通过对象名来访问,但建议通过类名访问它。
  • 类方法:被static修饰的成员方法叫类方法(静态方法)。类方法属于类,可以通过类名访问,也可以通过对象名访问,建议通过类名访问它。
  • 静态块:被static修饰的初始化块叫静态初始化块。静态块属于类,它在类加载的时候被隐式调用一次,之后便不会被调用了。
  • 静态内部类:被static修饰的内部类叫静态内部类。静态内部类可以包含静态成员,也可以包含非静态成员。静态内部类不能访问外部类的实例成员,只能访问外部类的静态成员。外部类的所有方法、初始化块都能访问其内部定义的静态内部类。
    final关键字可以修饰类、方法、变量,以下是final修饰这3种目标时表现出的特征:
  • final类:final关键字修饰的类不可以被继承。
  • final方法:final关键字修饰的方法不可以被重写。
  • final变量:final关键字修饰的变量,一旦获得了初始值,就不可以被修改。

String类可以被继承吗

在Java中,String类是被声明为final的,即不可被继承的,因此不能继承String类。
在Java中,被声明为final的类,不能被其他类所继承,也就是说,这个类不能拥有子类。另外,被声明为final的方法,也不能被子类所覆盖重写。而String类是Java中使用最广泛的类之一,因为String类代表了字符串,是一种不可变对象。由于字符串是Java中使用频率极高的类型之一,因此限制其被继承也是为了确保其性能、安全和可靠性。

接口和抽象类的区别

从设计目的上来说,二者有如下的区别:
接口体现的是一种规范。对于接口的实现者而言,接口规定了实现者必须向外提供哪些服务;对于接口的调用者而言,接口规定了调用者可以调用哪些服务,以及如何调用这些服务。当在一个程序中使用接口时,接口是多个模块间的耦合标准;当在多个应用程序之间使用接口时,接口是多个程序之间的通信标准。
抽象类体现的是一种模板式设计。抽象类作为多个子类的抽象父类,可以被当成系统实现过程中的中间产品,这个中间产品已经实现了系统的部分功能,但这个产品依然不能当成最终产品,必须有更进一步的完善,这种完善可能有几种不同方式。
从使用方式上来说,二者有如下的区别:

  • 接口里只能包含抽象方法、静态方法、默认方法和私有方法,不能为普通方法提供方法实现;抽象类则完全可以包含普通方法。抽象类中可以有构造方法,而接口中没有
  • 抽象类中的成员可以有不同的访问修饰符,而接口中的成员只能是public的
  • 接口里只能定义静态常量,不能定义普通成员变量;抽象类里则既可以定义普通成员变量,也可以定义静态常量。
  • 接口里不包含构造器;抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象,而是让其子类调用这些构造器来完成属于抽象类的初始化操作。
  • 接口里不能包含初始化块;但抽象类则完全可以包含初始化块。
  • 一个类最多只能有一个直接父类,包括抽象类;但一个类可以直接实现多个接口,通过实现多个接口可以弥补Java单继承的不足(一个类只能继承一个抽象类,而一个类可以实现多个接口)

说下冒泡排序的实现思路

冒泡排序是一种简单的排序算法,通过重复比较相邻的两个元素,将较大(较小)的元素放在最后面,达到排序的目的。代码:

public static void bubbleSort(int[] arr) {
    
    
    int n = arr.length;
    boolean swapped; //设置一个标志位,用于减少比较次数
    for (int i = 0; i < n - 1; i++) {
    
    
        swapped = false;
        for (int j = 0; j < n - 1 - i; j++) {
    
    
            if (arr[j] > arr[j + 1]) {
    
    
                //将较大的数交换到后面
                int temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
                swapped = true;
            }
        }
        if (!swapped) {
    
     //如果本轮没有发生交换,则说明已经有序,退出循环
            break;
        }
    }
}

在代码中,我们首先获取数组的长度n,然后通过两层循环来完成排序。内层循环用于比较相邻的元素,将较大的元素交换到后面。外层循环用于控制排序的次数,每轮排序将会有一个元素到达最终的位置,因此需要进行n-1轮排序。同时添加一个标志位swapped,在内层循环中,如果没有发生交换,则说明数组已经有序,无需继续比较,否则继续下一轮排序。

设计模式有哪些好处,为什么要用设计模式?

使用设计模式有以下好处:

  1. 代码重用:设计模式通过提供经过验证和可复用的解决方案,可以减少代码的重复编写。模式可以在不同的场景中使用,避免重新实现相似的功能。

  2. 可维护性:设计模式强调代码的组织和结构,使代码更易于理解和维护。模式可以提供清晰的抽象和分离,使代码更具可读性和可扩展性。

  3. 可测试性:设计模式通常将代码分解为更小的、可测试的单元,从而方便进行单元测试和集成测试。模式可以降低代码的耦合度,使测试更容易实施。

  4. 灵活性和可扩展性:设计模式通过解耦和抽象的方式,使系统更加灵活和可扩展。模式可以使系统更容易适应变化和扩展需求,减少对现有代码的修改。

  5. 高效性:设计模式是经过优化和验证的解决方案,可以提高代码的执行效率和性能。模式可以避免不必要的计算和资源消耗,使系统更高效地运行。

  6. 设计沟通和共享:设计模式提供了共享的词汇和概念,可以促进团队成员之间的沟通和理解。模式可以成为团队共享经验和知识的基础,提高团队协作和开发效率。

使用设计模式可以提供代码重用、可维护性、可测试性、灵活性、可扩展性、高效性以及设计沟通和共享等好处。设计模式是经过实践验证的最佳实践,可以帮助开发人员设计出可靠、可扩展和易于维护的软件系统。

讲讲单例设计模式(饿汉模式、懒汉模式)

单例模式能够确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一的实例
特点:

  • 提供一个自身类型的静态私有成员变量instance
  • 构造函数为私有,以确保在类外无法使用new创建实例
  • 提供一个公有的静态方法getInstance(),用于获取单例类的实例对象

饿汉模式是单例模式的一种实现方式。饿汉模式,饿汉模式顾名思义就是太饿了。在饿汉模式中,实例在类加载的时候就被创建并初始化,即在类定义的时候就已经创建了实例对象。这样可以确保在任何使用该实例的地方都能获取到同一个实例。饿汉模式的实现比较简单,但是可能会造成资源的浪费,因为实例在整个应用的生命周期中都被创建并占用内存,无论是否被使用。

懒汉模式 是单例模式的另一种实现方式。懒汉模式太懒了。在懒汉模式中,实例在第一次被使用时才会被创建。即在需要获取实例时才进行实例化操作。懒汉模式避免了资源的浪费,但需要考虑线程安全性的问题。如果多个线程同时访问实例的时候,可能会导致创建多个实例的问题。因此在懒汉模式中需要考虑线程安全的实现方式,例如使用 synchronized 关键字或者双重检查锁定(Double-Checked Locking)来保证只创建一个实例。

redis集群用过吗 常用的数据类型

Redis集群是指将多个Redis实例组成一个集群,共同提供高可用性、高性能和可扩展性的解决方案。Redis集群通过数据分片和主从复制的方式实现数据的分布和备份,确保数据的可靠性和可用性。

在Redis集群中,数据被分散存储在多个节点上,每个节点负责存储其中一部分数据。集群通过哈希槽(hash slot)将数据分片到不同的节点上。通常情况下,Redis集群由多个主节点和多个从节点组成,每个主节点负责一部分哈希槽的数据,并有一个或多个从节点进行数据备份和读取。

Redis集群提供了以下优势:

  1. 高可用性:当某个节点发生故障时,其他节点可以接管服务并继续提供服务,从而保证系统的可用性。
  2. 扩展性:通过添加新的节点,可以水平扩展Redis集群的存储和处理能力,以满足不断增长的需求。
  3. 数据分片:将数据分布在多个节点上,提供更好的负载均衡和性能。
  4. 数据备份:每个主节点都有对应的从节点进行数据备份,提供数据的冗余和容错能力。

需要注意的是,Redis集群需要使用Redis Cluster作为集群管理工具,通过节点之间的协调和通信来实现数据的分片和复制。

Redis支持以下几种主要的数据类型:

  1. 字符串(String):字符串是Redis中最基本的数据类型,可以存储任何类型的数据,如文本、数字等。

  2. 列表(List):列表是一个有序的字符串集合,可以按照插入顺序存储多个元素。可以通过索引访问、插入和删除元素,还提供了一些针对列表的操作,如范围获取、插入、删除等。

  3. 哈希(Hash):哈希是一个键值对的集合,其中每个键都对应一个值。哈希适合用于存储对象,可以通过键名快速访问和修改值。

  4. 集合(Set):集合是一组唯一且无序的字符串集合,不允许重复元素。可以对集合进行添加、删除、查找等操作,还提供了集合的交集、并集、差集等操作。

  5. 有序集合(Sorted Set):有序集合是一个有序的字符串集合,每个成员都关联一个分数,通过分数可以对成员进行排序。可以按照成员或者分数的范围进行检索,还提供了添加、删除、更新成员分数等操作。

看你简历上了解缓存穿透和缓存雪崩,能讲讲吗?

缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

常见的解决方案有两种:

  • 缓存空对象
    • 优点:实现简单,维护方便
    • 缺点:
      • 额外的内存消耗
      • 可能造成短期的不一致
  • 布隆过滤
    • 优点:内存占用较少,没有多余key
    • 缺点:
      • 实现复杂
      • 存在误判可能

**缓存空对象思路分析:**当我们客户端访问不存在的数据时,先请求redis,但是此时redis中没有数据,此时会访问到数据库,但是数据库中也没有数据,这个数据穿透了缓存,直击数据库,我们都知道数据库能够承载的并发不如redis这么高,如果大量的请求同时过来访问这种不存在的数据,这些请求就都会访问到数据库,简单的解决方案就是哪怕这个数据在数据库中也不存在,我们也把这个数据存入到redis中去,这样,下次用户过来访问这个不存在的数据,那么在redis中也能找到这个数据就不会进入到缓存了

**布隆过滤:**布隆过滤器其实采用的是哈希思想来解决这个问题,通过一个庞大的二进制数组,走哈希思想去判断当前这个要查询的这个数据是否存在,如果布隆过滤器判断存在,则放行,这个请求会去访问redis,哪怕此时redis中的数据过期了,但是数据库中一定存在这个数据,在数据库中查询出来这个数据后,再将其放入到redis中,

假设布隆过滤器判断这个数据不存在,则直接返回

这种方式优点在于节约内存空间,存在误判,误判原因在于:布隆过滤器走的是哈希思想,只要哈希思想,就可能存在哈希冲突
在这里插入图片描述
缓存穿透产生的原因是什么?

  • 用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求,给数据库带来巨大压力

缓存穿透的解决方案有哪些?

  • 缓存null值
  • 布隆过滤
  • 增强id的复杂度,避免被猜测id规律
  • 做好数据的基础格式校验
  • 加强用户权限校验
  • 做好热点参数的限流

缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:

  • 给不同的Key的TTL添加随机值
  • 利用Redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

缓存击穿

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。

常见的解决方案有两种:

  • 互斥锁
  • 逻辑过期

逻辑分析:假设线程1在查询缓存之后,本来应该去查询数据库,然后把这个数据重新加载到缓存的,此时只要线程1走完这个逻辑,其他线程就都能从缓存中加载这些数据了,但是假设在线程1没有走完的时候,后续的线程2,线程3,线程4同时过来访问当前这个方法, 那么这些线程都不能从缓存中查询到数据,那么他们就会同一时刻来访问查询缓存,都没查到,接着同一时间去访问数据库,同时的去执行数据库代码,对数据库访问压力过大
在这里插入图片描述
解决方案一、使用锁来解决:

因为锁能实现互斥性。假设线程过来,只能一个人一个人的来访问数据库,从而避免对于数据库访问压力过大,但这也会影响查询的性能,因为此时会让查询的性能从并行变成了串行,我们可以采用tryLock方法 + double check来解决这样的问题。

假设现在线程1过来访问,他查询缓存没有命中,但是此时他获得到了锁的资源,那么线程1就会一个人去执行逻辑,假设现在线程2过来,线程2在执行过程中,并没有获得到锁,那么线程2就可以进行到休眠,直到线程1把锁释放后,线程2获得到锁,然后再来执行逻辑,此时就能够从缓存中拿到数据了。
在这里插入图片描述
解决方案二、逻辑过期方案

方案分析:我们之所以会出现这个缓存击穿问题,主要原因是在于我们对key设置了过期时间,假设我们不设置过期时间,其实就不会有缓存击穿的问题,但是不设置过期时间,这样数据不就一直占用我们内存了吗,我们可以采用逻辑过期方案。

我们把过期时间设置在 redis的value中,注意:这个过期时间并不会直接作用于redis,而是我们后续通过逻辑去处理。假设线程1去查询缓存,然后从value中判断出来当前的数据已经过期了,此时线程1去获得互斥锁,那么其他线程会进行阻塞,获得了锁的线程他会开启一个 线程去进行 以前的重构数据的逻辑,直到新开的线程完成这个逻辑后,才释放锁, 而线程1直接进行返回,假设现在线程3过来访问,由于线程线程2持有着锁,所以线程3无法获得锁,线程3也直接返回数据,只有等到新开的线程2把重建数据构建完后,其他线程才能走返回正确的数据。

这种方案巧妙在于,异步的构建缓存,缺点在于在构建完缓存之前,返回的都是脏数据。
在这里插入图片描述
利用互斥锁解决缓存击穿问题
核心思路:相较于原来从缓存中查询不到数据后直接查询数据库而言,现在的方案是 进行查询之后,如果从缓存没有查询到数据,则进行互斥锁的获取,获取互斥锁后,判断是否获得到了锁,如果没有获得到,则休眠,过一会再进行尝试,直到获取到锁为止,才能进行查询

如果获取到了锁的线程,再去进行查询,查询后将数据写入redis,再释放锁,返回数据,利用互斥锁就能保证只有一个线程去执行操作数据库的逻辑,防止缓存击穿
利用逻辑过期解决缓存击穿问题

熔断降级/限流怎么做?

熔断、降级和限流是在分布式系统中常用的应对高并发、故障和资源限制的策略,它们的具体实现方式如下:

  1. 熔断(Circuit Breaking):熔断机制用于防止故障在分布式系统中蔓延,以保护系统的可用性。当系统出现故障或异常时,熔断器会迅速切断对该服务的访问,并在一段时间内拒绝请求。当熔断器开启时,可以选择直接返回错误响应或者返回预设的降级响应,避免资源的进一步浪费。

  2. 降级(Fallback):降级是在系统资源紧张或服务不可用时,通过提供替代方案来保证系统的可用性和用户体验。通过预设的降级逻辑,可以提供基本功能或者返回预先定义的响应数据,避免完全的系统崩溃。

  3. 限流(Rate Limiting):限流机制用于控制系统的并发访问量,防止系统被过多的请求压垮。通过限制每秒钟的请求数、连接数或者并发数,可以有效地保护系统的稳定性和资源的合理分配。常见的限流算法有令牌桶算法和漏桶算法。

实现熔断、降级和限流可以使用各种技术和工具,包括但不限于:

  • 熔断:常用的熔断器工具有Hystrix、Resilience4j等,它们提供了熔断器的配置和管理功能。
  • 降级:可以通过在代码中编写降级逻辑,或者使用降级工具库,如Sentinel等。
  • 限流:可以使用限流算法实现,也可以使用限流工具库,如Guava的RateLimiter、Redis的计数器等。

综合使用熔断、降级和限流策略,可以提高系统的稳定性、可用性和用户体验,有效应对高并发、故障和资源限制等场景。具体的实现方式要根据系统的需求和技术栈选择合适的工具和策略。

武汉超星最后几个问题有点难,而且这个是我的处女面,表现的有点紧张,不出意外挂了!!!

猜你喜欢

转载自blog.csdn.net/m0_56653797/article/details/131196617