Fork me on GitHub

Java的自动拆装箱

原文来自 Java的自动拆装箱

概念

​ 在说到拆箱和装箱之前,需要了解Java中有八种基本的数据类型,分别是:byte、short、char、int、long、float、double和boolean。这八种基本类型在Java中都有对应的包装类型:Byte、Short、Character、Integer、Long、Float、Double以及Boolean。

有了基本类型为什么还需要包装类型呢?这是由Java本身的语言特性决定的,Java是一种面向对象的编程语言,在学习Java之初就被明确灌输了一个概念:OOP,即面向对象编程。一切皆对象。但是基本类型是不具备Java中对象的某些特征,对象内部可以封装一系列属性和行为,但是这些在基本数据类型中都无法满足,所以对应的包装类型就应运而生了。作者:still_loving链接:https://www.jianshu.com/p/cc9312104876來源:简书简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

这里的装箱和拆箱的概念描述的其实就是Java中这八种基本数据类型和对应的包装类型之间的转换过程。我们把基本数据类型转换成对应的包装类型的过程叫做装箱。反之就是拆箱。在Java中的装箱和拆箱不是人为操作的,是程序在编译的时候编译器帮助我们完成这项任务的,因此说它是自动的。

需要明确的是自动拆装箱是在JDK1.5以后引入的,对于之前版本的Java,需要格外注意格式的转换。

为何需要自动装箱和拆箱?

方便

​ 首先就是方便程序员的编码,我们在编码过程中,可以不需要考虑包装类和基本类型之间的转换操作,这一步由编译器自动替我们完成,开发人员可以有更多的精力集中与具体的业务逻辑。否则的话,一个简单的数字赋值给包装类就得写两句代码,即:首先生成包装类型对象,然后将对象转换成基本数据类型。而这种操作是代码中使用频率很高的操作,导致代码书写量增多。

节约空间

​ 我们在查阅对应包装类的源代码时可以看到,大部分包装类型的valueOf方法都会有缓存的操作,即:将某段范围内的数据缓存起来,创建时如果在这段范围内,直接返回已经缓存的这些对象,这样保证在一定范围内的数据可以直接复用,而不必要重新生成。

​ 这么设计的目的因为:小数字的使用频率很高,将小数字缓存起来,让其仅有一个对象,可以起到节约存储空间的作用。这里其实采用的是一种叫做享元模式的设计模式。可以去具体了解以下这种设计模式,这里就不再过多赘述。

实现原理

​ Java中是怎么实现这个自动装箱和拆箱的过程的呢?这里需要借助与一些反编译工具,例如javap命令或者其他一些反编译的工具,我这里使用的是idea的bytecode插件,如果需要,可以到这里下载。在它的release中直接下载zip压缩包就行,然后作为插件安装在idea中就行,安装完成重启idea后,在需要反编译的java代码中右键,可以找到”Show Bytecode outline-dev”菜单选项,直接点击就可以看到反编译后的代码。

装箱

首先看下面两句代码:

1
2
Integer i = 20;
int j = 2;

在进行反编译之后可以得到:

1
2
Integer i = Integer.valueOf((int)10);
int j = 2;

可以看到对于数值类型直接赋值给包装类型,有一个自动装箱的操作,而自动装箱的操作就是利用了Integer中的valueOf方法,这就是前面在节约空间那部分提到的valueOf方法。Integer的valueOf方法中具有缓存的功能,也就是说在数值为-128到127之间的数据,都是被构造成同一个对象,这就是上面提到的享元模式的设计思路:

1
2
3
4
5
6
public static Integer valueOf(int i) {
//IntegerCache.low = -128, IntegerCache.high = 127
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

这个概念在刷面试题的时候,都被强调烂了,基本常见的笔试题目就是比较几个integer对象之间==操作:

1
2
3
4
5
6
Integer a = 100;
Integer b = 100;
Integer c = 128;
Integer d = 128;
System.out.println(a == b); //true
System.out.println(c == d); //false

​ 注意:也可以使用new Integer(num)的方式创建Integer对象,但是在JDK1.9之后,这个构造方法被标记为Deprecated,也就是过时了,所以以后尽量不要使用这种方式创建对象。它的注释中建议使用valueOf进行构建对象。利用构造器构造出来的对象不会经过取缓存操作,所以对于new Integer(100)的操作,得到的Integer对象与a或b进行==比较时,得到的会是false。

​ 其实其他七种包装类型的valueOf方法大多都是这个享元设计模式的逻辑,但是有两个除外:Float和Double。这个其实也很好理解:因为Integer这种类型的数据,-128到127之间的数据是有限个,总共就256个数字,但是对于Float和Double这种类型,它们之间的数据个数就无法计算了,所以它两个就没有采用这种缓存的方式。下面是其他包装类型中的valueOf方法的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//Short
public static Short valueOf(short s) {
final int offset = 128;
int sAsInt = s;
if (sAsInt >= -128 && sAsInt <= 127) { // must cache
return ShortCache.cache[sAsInt + offset];
}
return new Short(s);
}
//Byte
public static Byte valueOf(byte b) {
final int offset = 128;
return ByteCache.cache[(int)b + offset];
}
//Character
public static Character valueOf(char c) {
if (c <= 127) { // must cache
return CharacterCache.cache[(int)c];
}
return new Character(c);
}
//Long
public static Long valueOf(long l) {
final int offset = 128;
if (l >= -128 && l <= 127) { // will cache
return LongCache.cache[(int)l + offset];
}
return new Long(l);
}
//Boolean
public static Boolean valueOf(boolean b) {
//public static final Boolean TRUE = new Boolean(true);
//public static final Boolean FALSE = new Boolean(false);
return (b ? TRUE : FALSE);
}
//Float
public static Float valueOf(float f) {
return new Float(f);
}
//Double
public static Double valueOf(double d) {
return new Double(d);
}

通过上面的代码截图可以看到,对于Float和Double都是直接使用了构造器直接构造对应包装类型的对象。对于Boolean类型,就是固定的两个TRUE和FALSE两个常量,它们不会出现变化,这也属于一种缓存。

对于Byte类型,它是直接全部缓存了,这里使用了cache数组,它在Byte类中定义和初始化如下:

1
2
3
4
5
static final Byte cache[] = new Byte[-(-128) + 127 + 1];
static {
for(int i = 0; i < cache.length; i++)
cache[i] = new Byte((byte)(i - 128));
}

​ 所以cache数组中存储的就是-128到127范围的所有数。在构建时直接定位到具体的数组位置中去,并将该位置上的数值直接返回即可。

​ 其余数据类型基本逻辑都差不多了,都有一个缓存值范围,如果超过了,就利用构造器直接构造,否则直接返回缓存的对象。

拆箱

上面介绍的valueOf方法是装箱操作的时候使用的,还有一个拆箱操作,看下面这个例子:

1
2
3
Integer a = 100;
int b = 20;
int c = a + b;

上面代码反编译之后就得到:

1
2
3
Integer a = Integer.valueOf((int)100);
int b = 20;
int c = a.intValue() + b;

​ 可以看到第一步进行了自动装箱操作,在第三行中,基本数据类型和包装类型进行运算,需要将包装类型进行拆箱操作,用到了intValue方法。这个方法其实在源码中很简单,就是一句话,返回value。我们知道任何包装类型,内部都有一个基本数据类型的字段用于存储对应基本类型的值,这个字段就是value。

​ 相应的其他包装类型在进行拆箱的时候,都会调用对应的xxxValue方法,例如:byteValue、shortValue等等方法。其实内部逻辑都是一样,直接返回存储的value值。

自动装箱和拆箱的时机

直接赋值

​ 这个情况其实在前面介绍自动装箱的操作的时候,举例代码中就是这种情况,将一个字面量直接赋值给对应包装类型会触发自动装箱操作。

函数参数

1
2
3
4
5
6
7
8
//自动拆箱
public int getNum1(Integer num) {
return num;
}
//自动装箱
public Integer getNum2(int num) {
return num;
}

集合操作

​ 在Java的集合中,泛型只能是包装类型,但是我们在存储数据的时候,一般都是直接存储对应的基本类型数据,这里就有一个自动装箱的过程。

运算符运算

​ 上面在拆箱操作的时候利用的就是这个特性,当基本数据类型和对应的包装类型进行算术运算时,包装类型会首先进行自动拆箱,然后再与基本数据类型的数据进行运算。

1
2
Integer a = null;
int b = a;// int b = a.intValue();

这种情况编译是可以通过的,但是在运行的时候会抛出空指针异常,这就是自动拆箱导致的这种错误。因为自动拆箱会调用intValue方法,但是此时a是null,所以会抛异常。平时在使用的时候,注意非空判断即可。

自动装拆箱带来的问题

==比较

​ 首先就是前面提到的关于==操作符的结果问题,因为自动装箱的机制,我们不能依赖于==操作符,它在一定范围内数值相同为true,但是在更多的空间中,数值相同的包装类型对象比较的结果为false。如果需要比较,可以考虑使用equals比较或者将其转换成对应的基本类型再进行比较可以保证结果的一致性。

空指针

​ 这是上面在说到运算符的时候提到的一种情况,因为有自动拆箱的机制,如果初始的包装类型对象为null,那么在自动拆箱的时候的就会报NullPointerException,在使用时需要格外注意,在使用之前进行非空判定,保证程序的正常运行。

内存浪费

这里有个例子:

1
2
3
4
Integer sum = 0;
for(int i=1000; i<5000; i++){
sum+=i;
}

​ 上面代码中的 sum+=i 这个操作其实就是拆箱再装箱的过程,拆箱过程是发生在相加的时候,sum本身是Integer,自动拆箱成int与 i 相加。将得到的结果赋值给sum的时候,又会进行自动装箱,所以上面的for循环体中一句话,在编译后会变为两句:

​ 所以在进行了5000次循环后,会出现大量的无用对象造成内容空间的浪费,同时加重了垃圾回收的工作量,所以在日常编码过程中需要格外注意,避免出现这种浪费现象。

方法重载问题

​ 最典型的就是ArrayList中出现的remove方法,它有remove(int index)和remove(Object obj)方法,如果此时恰巧ArrayList中存储的就是Integer元素,那么会不会出现混淆的情况呢?其实这个只需要做一个简单的测试就行:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void test(Integer num) {
System.out.println("Integer参数的方法被调用...");
}

public static void test(int num) {
System.out.println("int参数的方法被调用...");
}
public static void main(String[] args) {
int i = 2;
test(i); //int参数的方法被调用...
Integer j = 4;
test(j);//Integer参数的方法被调用...
}

所以可以发现,当出现这种情况的时候,是不会发生自动装箱和拆箱操作的。可以正常区分。

Java 利用枚举实现单例模式

原文地址 https://blog.csdn.net/yy254117440/article/details/52305175

引言

单例模式比较常见的实现方法有懒汉模式,DCL模式公有静态成员等,从Java 1.5版本起,单元素枚举实现单例模式成为最佳的方法。

Java枚举

基本用法

枚举的用法比较多,本文主要旨在介绍利用枚举实现单例模式的原理,所以这里也主要介绍一些相关的基础内容。
首先,枚举类似类,一个枚举可以拥有成员变量,成员方法,构造方法。先来看枚举最基本的用法:

1
2
3
enum Type{
A,B,C,D;
}

创建enum时,编译器会自动为我们生成一个继承自java.lang.Enum的类,我们上面的enum可以简单看作:

1
2
3
4
5
lass Type extends Enum{
public static final Type A;
public static final Type B;
...
}

对于上面的例子,我们可以把Type看作一个类,而把A,B,C,D看作类的Type的实例。
当然,这个构建实例的过程不是我们做的,一个enum的构造方法限制是private的,也就是不允许我们调用。

阅读更多...

int和Integer区别和联系

1、 int和Integer的比较

a) Integer是int的包装类,int则是java的一种基本数据类型

b) Integer变量必须实例化后才能使用,而int变量不需要

c) Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值

d) Integer的默认值是null,int的默认值是0

2、 int和int值比较

直接是数值的比较,等就等,不等就拉倒。

3、Integer和Integer的比较

两个对象的比较,从一开始的有缘到后面的无分,都是有定数的。

4、 int和Integer的比较

本自同根生,褪下华丽的霓裳我们还是一家人。

Java中的Switch对整型、字符型、字符串型的具体实现细节

原文 Java中的Switch对整型、字符型、字符串型的具体实现细节

Java 7中,switch的参数可以是String类型了,这对我们来说是一个很方便的改进。到目前为止switch支持这样几种数据类型:byte short int char String 。但是,作为一个程序员我们不仅要知道他有多么好用,还要知道它是如何实现的,witch对整型的支持是怎么实现的呢?对字符型是怎么实现的呢?String类型呢?有一点Java开发经验的人这个时候都会猜测switch对String的支持是使用equals()方法和hashcode()方法。那么到底是不是这两个方法呢?接下来我们就看一下,switch到底是如何实现的。

一、switch对整型支持的实现

下面是一段很简单的Java代码,定义一个int型变量a,然后使用switch语句进行判断。执行这段代码输出内容为5,那么我们将下面这段代码反编译,看看他到底是怎么实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class switchDemoInt {
public static void main(String[] args) {
int a = 5;
switch (a) {
case 1:
System.out.println(1);
break;
case 5:
System.out.println(5);
break;
default:
break;
}
}
}
//output 5

反编译后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class switchDemoInt
{
public switchDemoInt()
{
}
public static void main(String args[])
{
int a = 5;
switch(a)
{
case 1: // '\001'
System.out.println(1);
break;

case 5: // '\005'
System.out.println(5);
break;
}
}
}

我们发现,反编译后的代码和之前的代码比较除了多了两行注释以外没有任何区别,那么我们就知道,switch对int的判断是直接比较整数的值

阅读更多...

Integer用==比较时127相等128不相等的原因

原文地址 Java: Integer用==比较时127相等128不相等的原因

前言

这个几乎是Java 5引入自动装箱和自动拆箱后,很多人都会遇到(而且不止一次),而又完全摸不着头脑的坑。虽然已有很多文章分析了原因,但鉴于我这次还差点坑了同学,还是纪录下来长点记性。

问题描述

例一

来个简单点的例子

1
2
3
4
5
6
7
public static void main(String[] args) {
for (int i = 0; i < 150; i++) {
Integer a = i;
Integer b = i;
System.out.println(i + " " + (a == b));
}
}

i取值从0到150,每次循环ab的数值均相等,输出a == b。运行结果:

1
2
3
4
5
6
7
8
9
10
11
0 true
1 true
2 true
3 true
...
126 true
127 true
128 false
129 false
130 false
...
阅读更多...

两年摸爬滚打 Spring Boot,总结了这 16 条最佳实践

Spring Boot是最流行的用于开发微服务的Java框架。在本文中,我将与你分享自2016年以来我在专业开发中使用Spring Boot所采用的最佳实践。这些内容是基于我的个人经验和一些熟知的Spring Boot专家的文章。

在本文中,我将重点介绍Spring Boot特有的实践(大多数时候,也适用于Spring项目)。以下依次列出了最佳实践,排名不分先后。

1、使用自定义BOM来维护第三方依赖

这条实践是我根据实际项目中的经历总结出的。

Spring Boot项目本身使用和集成了大量的开源项目,它帮助我们维护了这些第三方依赖。但是也有一部分在实际项目使用中并没有包括进来,这就需要我们在项目中自己维护版本。如果在一个大型的项目中,包括了很多未开发模块,那么维护起来就非常的繁琐。

怎么办呢?事实上,Spring IO Platform就是做的这个事情,它本身就是Spring Boot的子项目,同时维护了其他第三方开源库。我们可以借鉴Spring IO Platform来编写自己的基础项目platform-bom,所有的业务模块项目应该以BOM的方式引入。这样在升级第三方依赖时,就只需要升级这一个依赖的版本而已。

1
2
3
4
5
6
7
8
9
10
11
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.spring.platform</groupId>
<artifactId>platform-bom</artifactId>
<version>Cairo-SR3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

2、使用自动配置

Spring Boot的一个主要特性是使用自动配置。这是Spring Boot的一部分,它可以简化你的代码并使之工作。当在类路径上检测到特定的jar文件时,自动配置就会被激活。

使用它的最简单方法是依赖Spring Boot Starters。因此,如果你想与Redis进行集成,你可以首先包括:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

如果你想与MongoDB进行集成,需要这样:

阅读更多...

最常用DOS命令

命令 解释
1. cd 目录路径 进入一个目录
2. cd .. 进入父目录
3. dir 查看本目录下的文件和子目录列表
4. cls 清除屏幕命令
5. 上下键 查找屏幕敲过的命令
6. Tab键 自动补齐命令

Java 枚举相关问题

原文链接 Java 枚举(Part 2)相关面试题

问:Java 枚举类可以继承其他类(或实现其他接口)或者被其他类继承吗,为什么?

答:枚举类可以实现其他接口但不能继承其他类,因为所有枚举类在编译后的字节码中都继承自 java.lang.Enum(由编译器添加),而 Java 不支持多继承,所以枚举类不可以继承其他类。

枚举类不可以被继承,因为所有枚举类在编译后的字节码中都是继承自 java.lang.Enum(由编译器添加)的 final class 类,final 的类是不允许被派生继承的。(不清楚的可以查看前一篇历史推送枚举原理题)

问:Java switch 为什么能使用枚举类型?

答:Java 1.7 之前 switch 参数可用类型为 short、byte、int、char,枚举类型之所以能使用其实是编译器层面实现的,编译器会将枚举 switch 转换为类似 switch(s.ordinal()) { case Status.START.ordinal() } 形式,所以实质还是 int 参数类型,感兴趣的可以自己写个使用枚举的 switch 代码然后通过 javap -v 去看下字节码就明白了。

此问题延伸出一个新问题就是 JDK 1.7 中 switch 支持 String 类型参数的原理是什么?

实际上 JDK1.7 的 switch 支持 String 也是在编译器层面实现的,在 Java 虚拟机和字节代码层面上依然只支持在 switch 语句中使用与整数类型兼容的类型。我们在 switch 中使用的 String 类型在编译的过程中会将字符串类型转换成与整数类型兼容的格式(譬如基于字符串常量的哈希码等),不同的 Java 编译器可能采用不同的方式和优化策略来完成这个转换。

问:Java 枚举会比静态常量更消耗内存吗?

答:会更消耗,一般场景下不仅编译后的字节码会比静态常量多,而且运行时也会比静态常量需要更多的内存,不过这个多取决于场景和枚举的规模等等,不能明确的定论多多少(一般都至少翻倍以上),此外更不能因为多就一刀切的认为静态常量应该优于枚举使用,枚举有自己的特性和场景,优化也不能过度。我们在上一篇枚举实质原理中已经解释了每个枚举类中的具体枚举类型都是对应类中的一个静态常量,该常量在 static 块中被初始实例化,此外枚举类还有自己的一些特有方法,而静态常量实质却很简单,所以从对象占用内存大小方面来计算肯定是枚举类比静态常量更加占体积和消耗运行时内存,至于具体怎么算其实很简单,大家可以自己下去搜一下 java 对象占用内存大小即可了解更多,搞清楚特定场合下具体大多少没有什么实际意义,搞清楚为什么大和怎么算出来的本质原因即可。

Java8数组和List相互转换

原文 Java8数组和List相互转换

转换数组为List

1.使用Stream中的Collector收集器,代码:

1
2
String[] arrays = new String[]{"a", "b", "c"};
List<String> listStrings = Stream.of(arrays).collector(Collectors.toList());

2.使用java.util.Arrays工具类中的asList()方法(这个不是Java8中新增的内容):

1
2
String[] arrays = new String[]{"a", "b", "c"};
List<String> listStrings = Arrays.asList(arrays);

转换List为数组

1.使用Stream:

1
String[] ss = listStrings.stream().toArray(String[]::new);

2.使用List中的toArray()方法

1
String[] sss = listStrings.toArray(new String[listStrings.size()]);

Token 认证的来龙去脉

原文 Token 认证的来龙去脉

不久前,我在在前后端分离实践中提到了基于 Token 的认证,现在我们稍稍深入一些。

通常情况下,我们在讨论某个技术的时候,都是从问题开始。那么第一个问题:

为什么要用 Token?

而要回答这个问题很简单——因为它能解决问题!

可以解决哪些问题呢?

  1. Token 完全由应用管理,所以它可以避开同源策略
  2. Token 可以避免 CSRF 攻击
  3. Token 可以是无状态的,可以在多个服务间共享

Token 是在服务端产生的。如果前端使用用户名/密码向服务端请求认证,服务端认证成功,那么在服务端会返回 Token 给前端。前端可以在每次请求的时候带上 Token 证明自己的合法地位。如果这个 Token 在服务端持久化(比如存入数据库),那它就是一个永久的身份令牌。

于是,又一个问题产生了:需要为 Token 设置有效期吗?

需要设置有效期吗?

对于这个问题,我们不妨先看两个例子。一个例子是登录密码,一般要求定期改变密码,以防止泄漏,所以密码是有有效期的;另一个例子是安全证书。SSL 安全证书都有有效期,目的是为了解决吊销的问题,对于这个问题的详细情况,来看看知乎的回答。所以无论是从安全的角度考虑,还是从吊销的角度考虑,Token 都需要设有效期。

阅读更多...
  • Copyrights © 2015-2023 高行行
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信