Java面试题

字符串常量池

String s = new String("Hello World");

上述语句JVM 会现在字符串常量池中找,如果有就不在字符串常量池中创建,只在堆上创建”Hello World“的字符串对象;反之就在字符串常量池和堆中都创建,然后返回堆中的对象地址给变量s。所以new 字符串对象时会创建两个对象,分别在字符串常量池和堆中。

String s = "Hello World";

Java 虚拟机会先在字符串常量池中查找有没有“Hello World”这个字符串对象,如果有,则不创建任何对象,直接将字符串常量池中这个“Hello World”的对象地址返回,赋给变量 s;如果没有,在字符串常量池中创建“Hello World”这个对象,然后将其地址返回,赋给变量 s。

String s = new String("Hello World");
String s1 = new String("Hello World");

按照我们之前的分析,这两行代码会创建三个对象,字符串常量池中一个,堆上两个。

再来看下面这个例子:

String s = "Hello World";
String s1 = "Hello World";

这两行代码只会创建一个对象,就是字符串常量池中的那个。

JDK8之前 字符串常量池方法区中,JDK8之后在堆中。

String.intern()

String s1 = new String("Hello World");
String s2 = s1.intern();
System.out.println(s1 == s2);

第一行代码,字符串常量池中会先创建一个“Hello World”的对象,然后堆中会再创建一个“Hello World”的对象,s1 引用的是堆中的对象

第二行代码,对 s1 执行 intern() 方法,该方法会从字符串常量池中查找“Hello World”这个字符串是否存在,此时是存在的,所以 s2 引用的是字符串常量池中的对象。 即输出为false

String s1 = new String("Hello") + new String("World");
String s2 = s1.intern();
System.out.println(s1 == s2);

第一行代码,会在字符串常量池中创建两个对象,一个是"Hello",一个是"World",然后在堆中会创建两个匿名对象"Hello"和"World"(可以暂时忽略),最后还有一个"HelloWorld"的对象,s1 引用的是堆中"HelloWorld这个对象。

第二行代码,对 s1 执行 intern() 方法,该方法会从字符串常量池中查找"HelloWorld"这个对象是否存在,此时不存在的,但堆中已经存在了,所以字符串常量池中保存的是堆中这个"HelloWorld"对象的引用,也就是说,s2 和 s1 的引用地址是相同的,所以输出的结果为 true。

Java 7 之后呢,由于字符串常量池放在了堆中,执行 String.intern() 方法的时候,如果对象在堆中已经创建了,字符串常量池中就不需要再创建新的对象了,而是直接保存堆中对象的引用,也就节省了一部分的内存空间。

抽象类

abstract class AbstractPlayer {
} // 定义一个抽象类

特点:

  • 抽象类是不能实例化的,尝试通过 new 关键字实例化的话,编译器会报错,提示“类是抽象的,不能实例化”。但可以有子类。子类通过 extends 关键字来继承抽象类。

  • 如果一个类定义了一个或多个抽象方法,那么这个类必须是抽象类。当我们尝试在一个普通类中定义抽象方法的时候,编译器会有两处错误提示。第一处在类级别上,提示“这个类必须通过 abstract 关键字定义”,第二处在尝试定义 abstract 的方法上,提示“抽象方法所在的类不是抽象的”。

  • 抽象类中既可以定义抽象方法,也可以定义普通方法,抽象类中的抽象方法没有方法体。

public abstract class AbstractPlayer {
    abstract void play();
    
    public void sleep() {
        System.out.println("运动员也要休息而不是挑战极限");
    }
}

  • 抽象类派生的子类必须实现父类中定义的抽象方法。如果没有实现的话,编译器会提示“子类必须实现抽象方法”,除非该子类也是抽象类。

public class BasketballPlayer extends AbstractPlayer {
    @Override
    void play() {
        System.out.println("我是张伯伦,篮球场上得过 100 分");
    }
}

  • Java 原则上只支持单一继承,但通过接口可以实现多重继承的目的。

应用场景

  1. 当我们希望一些通用的功能被多个子类复用的时候,就可以使用抽象类。
  2. 当我们需要在抽象类中定义好 API,然后在子类中扩展实现的时候就可以使用抽象类。

接口

  • 接口中允许定义变量
  • 接口中允许定义抽象方法
  • 接口中允许定义静态方法(Java 8 之后)
  • 接口中允许定义默认方法(Java 8 之后)

接口通过 interface 关键字来定义,它可以包含一些常量和方法,来看下面这个示例。

public interface Electronic {
    // 常量
    String LED = "LED";

    // 抽象方法
    int getElectricityUse();

    // 静态方法
    static boolean isEnergyEfficient(String electtronicType) {
        return electtronicType.equals(LED);
    }

    // 默认方法
    default void printDescription() {
        System.out.println("电子");
    }
}

来看一下这段代码反编译后的字节码。

public interface Electronic
{

    public abstract int getElectricityUse();

    public static boolean isEnergyEfficient(String electtronicType)
    {
        return electtronicType.equals("LED");
    }

    public void printDescription()
    {
        System.out.println("\u7535\u5B50");
    }

    public static final String LED = "LED";
}

接口类在编译时会自动添加public关键字,接口中定义的变量会在编译的时候自动加上 public static final 修饰符。

没有使用 privatedefault 或者 static 关键字修饰的方法是隐式抽象的,在编译的时候会自动加上 public abstract 修饰符。

接口中定义静态方法的目的是为了提供一种简单的机制,使我们不必创建对象就能调用方法,从而提高接口的竞争力。

实现该接口而不覆盖该方法的类提供默认实现。既然要提供默认实现,就要有方法体,换句话说,默认方法后面不能直接使用“;”号来结束——编译器会报错。

为什么允许定义默认方法?

在多个实现类中保证某个具体方法不变,在没有 default 方法的帮助下,我们就必须挨个对实现类进行修改。

接口使用规范

  1. 接口不允许直接实例化,否则编译器会报错。必须定义实现类,然后再实例化。
  2. 接口可以是空的,既可以不定义变量,也可以不定义方法。
  3. 接口类不能使用final关键字
  4. 接口类的抽象方法不能是 privateprotected 或者 final,否则编译器都会报错。

接口可以实现多态

什么是多态呢?通俗的理解,就是同一个事件发生在不同的对象上会产生不同的结果。多态可以通过继承(extends)的关系实现,也可以通过接口的形式实现。

Shape 接口表示一个形状。

public interface Shape {
    String name();
}

Circle 类实现了 Shape 接口,并重写了 name() 方法。

public class Circle implements Shape {
    @Override
    public String name() {
        return "圆";
    }
}

Square 类也实现了 Shape 接口,并重写了 name() 方法。

public class Square implements Shape {
    @Override
    public String name() {
        return "正方形";
    }
}

然后来看测试类。

List<Shape> shapes = new ArrayList<>();
Shape circleShape = new Circle();
Shape squareShape = new Square();

shapes.add(circleShape);
shapes.add(squareShape);

for (Shape shape : shapes) {
    System.out.println(shape.name());
}

这就实现了多态,变量 circleShape、squareShape 的引用类型都是 Shape,但执行 shape.name() 方法的时候,Java 虚拟机知道该去调用 Circle 的 name() 方法还是 Square 的 name() 方法。

说一下多态存在的 3 个前提:

  • 1、要有继承关系,比如说 Circle 和 Square 都实现了 Shape 接口。
  • 2、子类要重写父类的方法,Circle 和 Square 都重写了 name() 方法。
  • 3、父类引用指向子类对象,circleShape 和 squareShape 的类型都为 Shape,但前者指向的是 Circle 对象,后者指向的是 Square 对象。

测试结果:

圆
正方形

也就意味着,尽管在 for 循环中,shape 的类型都为 Shape,但在调用 name() 方法的时候,它知道 Circle 对象应该调用 Circle 类的 name() 方法,Square 对象应该调用 Square 类的 name() 方法。

ArrayList中添加元素机制

在添加元素的时候会判断需不需要进行扩容,如果需要的话,会执行 grow() 方法进行扩容。

下面是 add(E e) 方法的源码:

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

调用了私有的 ensureCapacityInternal 方法:

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

假如一开始创建 ArrayList 的时候没有指定大小,elementData 就会被初始化成一个空的数组,也就是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA。

进入到 if 分支后,minCapacity 的值就会等于 DEFAULT_CAPACITY,可以看一下 DEFAULT_CAPACITY 的初始值:

private static final int DEFAULT_CAPACITY = 10;

也就是说,如果 ArrayList 在创建的时候没有指定大小,默认可以容纳 10 个元素。

接下来会进入 ensureExplicitCapacity 方法:

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

接着进入 grow(int minCapacity) 方法:

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1); 
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

然后对数组进行第一次扩容 Arrays.copyOf(elementData, newCapacity),由原来的 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 扩容为容量为 10 的数组。

“那假如向 ArrayList 添加第 11 个元素呢?”此时,minCapacity 等于 11,elementData.length 为10,ensureExplicitCapacity() 方法中 if 条件分支就起效了:

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

会再次进入到 grow() 方法:

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩容
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

“oldCapacity 等于 10,“>> 是右移运算符,oldCapacity >> 1 相当于 oldCapacity 除以 2。”

平常我们使用的是十进制数,比如说 39,并不是简单的 3 和 9,3 表示的是 3*10 = 30,9 表示的是 9*1 = 9,和 3 相乘的 10,和 9 相乘的 1,就是位权。位数不同,位权就不同,第 1 位是 10 的 0 次方(也就是 10^0=1),第 2 位是 10 的 1 次方(10^1=10),第 3 位是 10 的 2 次方(10^2=100),最右边的是第一位,依次类推。

位权这个概念同样适用于二进制,第 1 位是 2 的 0 次方(也就是 2^0=1),第 2 位是 2 的 1 次方(2^1=2),第 3 位是 2 的 2 次方(2^2=4),第 34 位是 2 的 3 次方(2^3=8)。

十进制的情况下,10 是基数,二进制的情况下,2 是基数。

10 在十进制的表示法是 0*10^0+1*10^1=0+10=10。

10 的二进制数是 1010,也就是 0*2^0 + 1*2^1 + 0*2^2 + 1*2^3=0+2+0+8=10。

然后是移位运算,移位分为左移和右移,在 Java 中,左移的运算符是 <<,右移的运算符 >>

拿 oldCapacity >> 1 来说吧,>> 左边的是被移位的值,此时是 10,也就是二进制 1010>> 右边的是要移位的位数,此时是 1。

1010 向右移一位就是 101,空出来的最高位此时要补 0,也就是 0101。

“那为什么不补 1 呢?”

“因为是算术右移,并且是正数,所以最高位补 0;如果表示的是负数,就需要补 1。”我慢吞吞地回答道,“0101 的十进制就刚好是 1*2^0 + 0*2^1 + 1*2^2 + 0*2^3=1+0+4+0=5,如果多移几个数来找规律的话,就会发现,右移 1 位是原来的 1/2,右移 2 位是原来的 1/4,诸如此类。”

也就是说,ArrayList 的大小会扩容为原来的大小+原来大小/2,也就是差不多 1.5 倍。

HashMap原理

hash原理

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

JDK8之后,hash方法是对象的hashcode值与其右移16位的数进行异或运算,

HashMap 扩容之前的数组初始大小只有 16,所以这个哈希值是不能直接拿来用的,用之前要和数组的长度做取模运算,用得到的余数来访问数组下标才行。取模运算有两处。

一处是往 HashMap 中 put 的时候(putVal 方法中):

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
     HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
     if ((tab = table) == null || (n = tab.length) == 0)
         n = (tab = resize()).length;
     if ((p = tab[i = (n - 1) & hash]) == null)
         tab[i] = newNode(hash, key, value, null);
}

一处是从 HashMap 中 get 的时候(getNode 方法中):

final Node<K,V> getNode(int hash, Object key) {
     Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
     if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {}
}

其中的 (n - 1) & hash 正是取模运算,就是把哈希值和(数组长度-1)做了一个“与”运算。

可能大家在疑惑:取模运算难道不该用 % 吗?为什么要用 & 呢

这是因为 & 运算比 % 更加高效,并且当 b 为 2 的 n 次方时,存在下面这样一个公式。

a % b = a & (b-1)

用 $2^n$ 替换下 b 就是:

a % $2^n$ = a & ($2^n$-1)

我们来验证一下,假如 a = 14,b = 8,也就是 $2^3$,n=3。

这也正好解释了为什么 HashMap 的数组长度要取 2 的整次方。因为(数组长度-1)正好相当于一个“低位掩码”——这个掩码的低位最好全是 1,这样 & 操作才有意义,否则结果就肯定是 0,那么 & 操作就没有意义了。

a&b 操作的结果是:a、b 中对应位同时为 1,则对应结果位为 1,否则为 0

2 的整次幂刚好是偶数,偶数-1 是奇数,奇数的二进制最后一位是 1,保证了 hash &(length-1) 的最后一位可能为 0,也可能为 1(这取决于 h 的值),即 & 运算后的结果可能为偶数,也可能为奇数,这样便可以保证哈希值的均匀性。

& 操作的结果就是将哈希值的高位全部归零,只保留低位值,用来做数组下标访问。

假设某哈希值为 10100101 11000100 00100101,用它来做取模运算,我们来看一下结果。HashMap 的初始长度为 16(内部是数组),16-1=15,二进制是 00000000 00000000 00001111(高位用 0 来补齐):

	 10100101 11000100 00100101
&	00000000 00000000 00001111
----------------------------------
	 00000000 00000000 00000101

因为 15 的高位全部是 0,所以 & 运算后的高位结果肯定是 0,只剩下 4 个低位 0101,也就是十进制的 5,也就是将哈希值为 10100101 11000100 00100101 的键放在数组的第 5 位。

 

HashMap原理

hash原理

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

JDK8之后,hash方法是对象的hashcode值与其右移16位的数进行异或运算,

HashMap 扩容之前的数组初始大小只有 16,所以这个哈希值是不能直接拿来用的,用之前要和数组的长度做取模运算,用得到的余数来访问数组下标才行。取模运算有两处。

一处是往 HashMap 中 put 的时候(putVal 方法中):

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
     HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
     if ((tab = table) == null || (n = tab.length) == 0)
         n = (tab = resize()).length;
     if ((p = tab[i = (n - 1) & hash]) == null)
         tab[i] = newNode(hash, key, value, null);
}

一处是从 HashMap 中 get 的时候(getNode 方法中):

final Node<K,V> getNode(int hash, Object key) {
     Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
     if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {}
}

其中的 (n - 1) & hash 正是取模运算,就是把哈希值和(数组长度-1)做了一个“与”运算。

可能大家在疑惑:取模运算难道不该用 % 吗?为什么要用 & 呢

这是因为 & 运算比 % 更加高效,并且当 b 为 2 的 n 次方时,存在下面这样一个公式。

a % b = a & (b-1)

用 $2^n$ 替换下 b 就是:

a % $2^n$ = a & ($2^n$-1)

我们来验证一下,假如 a = 14,b = 8,也就是 $2^3$,n=3。

这也正好解释了为什么 HashMap 的数组长度要取 2 的整次方。因为(数组长度-1)正好相当于一个“低位掩码”——这个掩码的低位最好全是 1,这样 & 操作才有意义,否则结果就肯定是 0,那么 & 操作就没有意义了。

a&b 操作的结果是:a、b 中对应位同时为 1,则对应结果位为 1,否则为 0

2 的整次幂刚好是偶数,偶数-1 是奇数,奇数的二进制最后一位是 1,保证了 hash &(length-1) 的最后一位可能为 0,也可能为 1(这取决于 h 的值),即 & 运算后的结果可能为偶数,也可能为奇数,这样便可以保证哈希值的均匀性。

& 操作的结果就是将哈希值的高位全部归零,只保留低位值,用来做数组下标访问。

假设某哈希值为 10100101 11000100 00100101,用它来做取模运算,我们来看一下结果。HashMap 的初始长度为 16(内部是数组),16-1=15,二进制是 00000000 00000000 00001111(高位用 0 来补齐):

	 10100101 11000100 00100101
&	00000000 00000000 00001111
----------------------------------
	 00000000 00000000 00000101

因为 15 的高位全部是 0,所以 & 运算后的高位结果肯定是 0,只剩下 4 个低位 0101,也就是十进制的 5,也就是将哈希值为 10100101 11000100 00100101 的键放在数组的第 5 位。


腾讯(阿里)云短信服务的实现机制

腾讯云短信云服务

导入依赖

   <dependency>
        <groupId>com.tencentcloudapi</groupId>
        <artifactId>tencentcloud-sdk-java</artifactId>
    </dependency>

在配置文件中配置

创建工具类

//实现了InitializingBean接口,当spring进行初始化bean时,会执行afterPropertiesSet方法
@Component
public class MsmConstantUtils implements InitializingBean {
	//我已经再
    @Value("${tencent.msm.id}")
    private String secretID ;

    @Value("${tencent.msm.secret}")
    private String secretKey ;

    @Value("${tencent.msm.endPoint}")
    private String endPoint;

    @Value("${tencent.msm.appId}")
    private String appId;

    @Value("${tencent.msm.signName}")
    private String signName;

    @Value("${tencent.msm.templateId}")
    private String templateId;
    //六个相关的参数
    public static String SECRET_ID;
    public static String SECRET_KEY;
    public static String END_POINT;
    public static String APP_ID;
    public static String SIGN_NAME;
    public static String TEMPLATE_ID;
 
    @Override
    public void afterPropertiesSet() throws Exception {
        SECRET_ID = secretID;
        SECRET_KEY = secretKey;
        END_POINT = endPoint;
        APP_ID = appId;
        SIGN_NAME = signName;
        TEMPLATE_ID = templateId;
    }
}

创建随机验证码类

public class RandomUtil {

    private static final Random random = new Random();
    //定义的验证码位数是6位
    private static final DecimalFormat sixdf = new DecimalFormat("000000");

    public static String getSixBitRandom() {
        return sixdf.format(random.nextInt(1000000));
    }
}

接口开发

Controller层

@RestController
@RequestMapping("/msm")
@CrossOrigin
@Api("发送短信服务")
public class MsmController {

    @Autowired
    private MsmService msmService;

    @ApiOperation("发送短信")
    @GetMapping("/send/{phone}")
    public ResponseEntity send(@PathVariable String phone) {

        boolean send = msmService.send(phone);
        if (send) {
            return ResponseEntity.ok();
        }
        return ResponseEntity.error();
    }
}

Service层

  • 通过自己认证的申请的密钥对实例化一个Credential对象
  • 实例化 HttpProfile 对象,调用etEndpoint()方法
  • 实例化ClientProfile对象,调用setHttpProfile()方法传入HttpProfile 对象
  • 实例化SmsClient对象,参数为(Credential对象, "当初选的地址", ClientProfile对象)
  • 实例化SendSmsRequest请求对象,开始发送请求(重点)
    setPhoneNumberSet 添加传进来的手机号
    setSmsSdkAppid 设置配置文件中的Appid
    setSign 设置当时申请时的签名名称;
    setTemplateID 设置验证码模板id;
  • 调用随机验证码工具类生成随机验证码对象
    调用SendSmsRequest 的 setTemplateParamSet()方法
  • 发送功能 SmsClient的SendSms(SendSmsRequest对象)

@Service
@Slf4j
public class MsmServiceImpl implements MsmService {
    @Override
    public boolean send(String phone) {
        try {
            //这里是实例化一个Credential,也就是认证对象,参数是密钥对;你要使用肯定要进行认证
            Credential credential = new Credential(MsmConstantUtils.SECRET_ID, MsmConstantUtils.SECRET_KEY);

            //HttpProfile这是http的配置文件操作,比如设置请求类型(post,get)或者设置超时时间了、还有指定域名了
            //最简单的就是实例化该对象即可,它的构造方法已经帮我们设置了一些默认的值
            HttpProfile httpProfile = new HttpProfile();
            //这个setEndpoint可以省略的
            httpProfile.setEndpoint(MsmConstantUtils.END_POINT);

            //实例化一个客户端配置对象,这个配置可以进行签名(使用私钥进行加密的过程),对方可以利用公钥进行解密
            ClientProfile clientProfile = new ClientProfile();
            clientProfile.setHttpProfile(httpProfile);

            //实例化要请求产品(以sms为例)的client对象
            SmsClient smsClient = new SmsClient(credential, "ap-beijing", clientProfile);

            //实例化request封装请求信息
            SendSmsRequest request = new SendSmsRequest();
            String[] phoneNumber = {phone};
            request.setPhoneNumberSet(phoneNumber);     //设置手机号
            request.setSmsSdkAppid(MsmConstantUtils.APP_ID);
            request.setSign(MsmConstantUtils.SIGN_NAME);
            request.setTemplateID(MsmConstantUtils.TEMPLATE_ID);
            //生成随机验证码,我的模板内容的参数只有一个
            String verificationCode = RandomUtil.getSixBitRandom();
            String[] templateParamSet = {verificationCode};
            request.setTemplateParamSet(templateParamSet);

            //发送短信
            SendSmsResponse response = smsClient.SendSms(request);
            log.info(SendSmsResponse.toJsonString(response));
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值