字符串常量池
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 原则上只支持单一继承,但通过接口可以实现多重继承的目的。
应用场景
- 当我们希望一些通用的功能被多个子类复用的时候,就可以使用抽象类。
- 当我们需要在抽象类中定义好 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
修饰符。
没有使用 private
、default
或者 static
关键字修饰的方法是隐式抽象的,在编译的时候会自动加上 public abstract
修饰符。
接口中定义静态方法的目的是为了提供一种简单的机制,使我们不必创建对象就能调用方法,从而提高接口的竞争力。
实现该接口而不覆盖该方法的类提供默认实现。既然要提供默认实现,就要有方法体,换句话说,默认方法后面不能直接使用“;”号来结束——编译器会报错。
为什么允许定义默认方法?
在多个实现类中保证某个具体方法不变,在没有 default
方法的帮助下,我们就必须挨个对实现类进行修改。
接口使用规范
- 接口不允许直接实例化,否则编译器会报错。必须定义实现类,然后再实例化。
- 接口可以是空的,既可以不定义变量,也可以不定义方法。
- 接口类不能使用final关键字
- 接口类的抽象方法不能是 private、protected 或者 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;
}
}
}