10. 补充其他常用类型
Java包装类型
查看下列代码,请回答下列代码的执行结果是什么?
包装类相关引例
int end = Integer.MAX_VALUE;
int start = end - 5;
public static void main(String[] args) {
int count = 0;
for (int i = start; i <= end; i++){
count++;
}
System.out.println(count);
}
首先查看一个经典报错:
Non-static field 'start' cannot be referenced from a static context
意译过来,就是在一个静态(static)的方法中无法访问一个非静态的成员变量。
我们早就学习过static关键字了,在static修饰的静态成员方法中,是不能直接访问类的成员变量的。
上述代码,可以在end和start两个变量的声明前,加上static就不会再报错了。
当然,这个题目更重要的一点在于:
有符号数的最大值:
- Integer.MAX_VALUE的二进制是0111 1111 1111 1111 1111 1111 1111 1111
- Integer.MIN_VALUE的二进制是 1000 0000 0000 0000 0000 0000 0000 0000
- Integer.MAX_VALUE + 1 = Integer.MIN_VALUE
所以很明显,上述代码,循环会从int类型最大值循环到int类型最小值,再循环到int类型最大值....这是一个死循环!
当然,让这个代码不死循环也很简单,只需要修改循环条件即可。
像上述代码中的类Integer其实就是包装类,下面具体讲解一下包装类型。
定义
什么是包装类?
大家都知道:Java是面向对象的语言,Java当中万物皆对象。
但这句话并不严谨,因为Java不是完全面向对象的编程语言。
因为Java还有基本数据类型变量,它们不是对象。
所以为了弥补基本数据类型变量非对象的尴尬境地,并且我们确有需求把基本数据类型变量也当成一个对象使用。
Java引入的技术。
所谓包装类,就是把基本数据类型包装成引用数据类型,变成一个个对象, 就可以调用类中的方法
以下包装类和八种基本数据类型对应关系:
包装类型对照表
基本数据类型 | 对应包装类 | 包装类的直接父类 |
---|---|---|
byte | java.lang.Byte | Number |
short | java.lang.Short | Number |
int | java.lang.Integer | Number |
long | java.lang.Long | Number |
float | java.lang.Float | Number |
double | java.lang.Double | Number |
boolean | java.lang.Boolean | Object |
char | java.lang.Character | Object |
继承关系
包装类型特点
包装类型对象不可变
所有数值包装类型和Boolean类,都是使用一个对应类型的value成员来存储它的基本数据类型变量的取值的。
比如Integer之于int类型,源码如下:
/** * The value of the {@code Integer}. * * @serial */ private final int value;
这个value都是final修饰的,这就意味着是修改不了的。
所有包装类型都是final修饰的,不能通过继承破坏value的设计体系。
总之,包装类型中有value成员变量的对象,都是不可变的。
基本数据类型与包装类型的相互转换
包装类型对象在使用上,在绝大多数情况下,是和它对应的基本数据类型是没有区别的。这依赖于Java"自动拆装箱"机制。
拆箱和装箱的概念:
- 在Java当中,把基本数据类型变量,转换为其对应包装类的引用数据类型变量,称之为"装箱"。
- 在Java当中,把包装类的引用数据类型类型变量,转换为其对应基本数据类型变量,称之为"拆箱"。
手动装修/拆箱与自动装箱/拆箱
// 手动装箱
int a =1;
Integer integer = new Integer(a);
Integer integer1 = Integer.valueOf(2);
// 手动拆箱
int i = integer.intValue();
// 自动装箱
Integer integer2 = 20;
// 自动拆箱
int n = integer2;
自动装箱与拆箱
自动装箱和拆箱其实是一种语法糖,在底层自动调用了方法而已:
自动装箱依赖于,包装类型类名.valueOf(对应基本数据类型值)
比如:
Integer i = Integer.valueOf(123);
在代码中出现上述写法,就会警告:
Unnecessary boxing 'Integer.valueOf(123)'
即装箱是自动,无需再手动调用方法。
自动拆箱依赖于,包装类型对象名.基本数据类型名Value()
比如:
int num = i.intValue();
在代码中出现上述写法,也一样会警告:
// Unnecessary unboxing 'i.intValue()'
即拆箱是自动,无需再手动调用方法。
包装类型和String类型的相互转换
包装类--->String
// wrapper----> String
Integer integer = 2; // 自动装箱
// 方式一: 使用toString方法
String s1 = integer.toString();
System.out.println("s1 = " + s1);
// 方式二: 字符串拼接
String s2 = "" + integer;
System.out.println("s2 = " + s2);
// 方式三: 使用String里的api: valueOf(int i)
String s3 = String.valueOf(integer);
System.out.println("s3 = " + s3);
String--->包装类
// String ---> wrapper
String s = "1";
// 方式一: parseInt(int i)
Integer integer1 = Integer.parseInt(s); // 自动装箱
System.out.println("integer = " + integer1);
// 方式二: valueOf(String s)
Integer integer2 = Integer.valueOf(s);
System.out.println("i1 = " + integer2);
// 方式三: 使用Integer的构造方法
Integer integer3 = new Integer(s);
System.out.println("integer2 = " + integer3);
包装类的常用方法
把String字符串转换成各种基本数据类型,普遍使用包装类型类名.parseXxx("字符串数值")
其中Xxx是对应基本数据类型
比如:
String --> int,使用Integer.parseInt("123")
String --> double,使用Double.parseDouble("0.1");
......
字符(Character)相关的
// 转换为小写
static char toLowerCase(char ch)
// 转换为大写
static char toUpperCase(char ch)
// 确定指定字符是否为大写字母
static boolean isUpperCase(char ch)
// 确定指定字符是否为小写字母
static boolean isLowerCase(char ch)
// 否为空格
static boolean isWhitespace(char ch)
// 确定指定字符是否为字母或数字
static boolean isLetterOrDigit(char ch)
// 确定指定字符是否为字母
static boolean isLetter(char ch)
// 确定指定字符是否为数字。
static boolean isDigit(char ch)
Integer的缓存机制
经典问题, 回答输出结果, 为什么?
public static void main(String[] args) {
Integer i1 = new Integer(1);
Integer i2 = new Integer(1);
System.out.println(i1 == i2);// false 对象地址不一样
Integer i3 = 1; // 底层使用valueOf(int i)方法
Integer i4 =1; // 底层使用valueOf(int i)方法
System.out.println(i3 == i4);// true
Integer i5 = 1; // 底层使用valueOf(int i)方法
Integer i6 =1;//底层使用valueOf(int i)方法
System.out.println(i5 == i6);// true
}
原因:
Integer源代码
Integer中有缓存, low=-128 high=127
范围-128-127 , 在这个范围内, 返回同一个
不在范围内, new一个新的对象返回
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
Integer的一些面试问题
回答输出结果, true 还是false ? 为什么?
Q1
Object obj = true ? new Integer(1):new Double(2.0);
System.out.println(obj);
// 1.0 三目运算符取决于最大范围的double
Q2
Object obj;
if(true)
obj = new Integer(1);
else
obj = new Double(2.0);
System.out.println(obj);
// 1 跟上面那个不一样,这是if语句
Q3
Integer i1 = new Integer(127);
Integer i2 = new Integer(127);
System.out.println(i1 == i2);
// false
Integer i3 = new Integer(128);
Integer i4 = new Integer(128);
System.out.println(i3 == i4);
// false 对象地址不一样
注意事项
包装类始终是一个类,它是一个引用数据类型,始终是不同于基本数据类型的,所以要注意以下:
- 使用包装类要注意空指针异常,而基本数据类型没有这个烦恼。
- 包装类对象在比较对象相等时,不能再像基本数据类型一样用"=="比较了!而是要使用"equals"方法。
在以上注意事项中,尤其注意不能使用"=="比较大小,这里我们做一下原理的讲解。
包装类型在比较对象相等时,要分三种情况:
- 整型包装类型对象之间的比较
- 浮点型包装类型对象之间的比较
- Boolean对象之间的比较
整型包装类型对象相等的比较
简单来说,整型的包装类型对象:
当它的取值在一个byte即[-128,127]的取值范围内时,会从缓存(cache)中共享同一个对象。
这样的话,这个范围的整型包装类型对象,用"=="判断就是true。
但是一旦它的取值超出了一个byte的取值范围,那么就会重新创建一个对象。
这样的话,用"=="判断就是false。
不同的整型包装类型对象,一个byte的取值范围的对象缓存生成时机是不同的:
- Integer对象是在JVM启动时就把对象放入缓存。
- 其余整型对象是在具体使用时把对象放入缓存。
浮点型包装类型对象相等的比较
浮点型包装类型对象是没有缓存存在的,查看浮点型包装类的valueOf方法可以发现:
Double的valueOf方法
public static Double valueOf(double d) {
return new Double(d);
}
所以浮点型包装类型如果直接用字面值赋值,一定会创建新对象,不能用"=="比较大小
Double d3 = 2.0;
Double d4 = 2.0;
System.out.println(d3 == d4);// false
布尔型对象相等的比较
查看Boolean包装类的valueOf方法可以发现:
Boolean的valueOf方法
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
那么TRUE和FALSE又是什么呢?
Boolean的全局常量
/**
* The {@code Boolean} object corresponding to the primitive
* value {@code true}.
*/
public static final Boolean TRUE = new Boolean(true);
/**
* The {@code Boolean} object corresponding to the primitive
* value {@code false}.
*/
public static final Boolean FALSE = new Boolean(false);
注:
建议包装类型对象比较内容还是使用equals方法, 不要使用==
使用场景
包装类的使用场景还是比较多的,最常见的:
- 包装类把基本数据类型变为一个对象,并且存在自动拆装箱,有些时候这本身就是一个用途
- 集合当中使用,集合只能存储对象,所以集合中就要用包装类型替代基本数据类型
- 获取一些最值之类的常量
- 做进制转换,类型转换等操作
- 后续做项目, 写接口的时候也建议使用包装类型(需要判断null)
Java枚举类型
引入
需求:
定义一个表示星期的类WeekDay, 定义2个属性
String name: 表示是周几
int id: 表示编号(1-7)
创建并打印相应的对象
class WeekDay{
String name;
int id;
public WeekDay(String name, int id) {
this.name = name;
this.id = id;
}
}
经过测试, 我们发现不仅能够创建周一到周日的对象, 还能够创建其他的对象, 但是实际上我们的需求只需要7个固定的对象, 其余的不需要, 不满足我们的需求
这种情况下,我们需要枚举类型(enumeration)(一个个的列举) , 一个特殊的类用来存放固定的几个常量对象(周一到周日)
枚举的两种实现方式
- 自定义类实现枚举
- 使用Enum关键字实现枚举
自定义类实现枚举
- 构造方法私有, 不允许外部创建对象, 类内部可以创建, 保证数量是固定的
- 枚举对象名通常使用大写
- 对枚举对象(属性)使用static final修饰, 保证是常量, 能够通过类名去访问(暴露给外部访问的一个入口)
- 不需要提供setXXX()方法, 枚举对象通常为只读
class WeekDay2{
public static final WeekDay2 MONDAY = new WeekDay2("周一", 1);
public static final WeekDay2 TUESDAY = new WeekDay2("周二", 2);
public static final WeekDay2 WEDNESDAY = new WeekDay2("周三", 3);
public static final WeekDay2 THURSDAY = new WeekDay2("周四", 4);
public static final WeekDay2 FRIDAY = new WeekDay2("周五", 5);
public static final WeekDay2 SATRUDAY = new WeekDay2("周六", 6);
public static final WeekDay2 SUNDAY = new WeekDay2("周日", 7);
private String name;
private int id;
// 构造方法私有
private WeekDay2(String name, int id) {
this.name = name;
this.id = id;
}
public String getName() {
return name;
}
public int getId() {
return id;
}
@Override
public String toString() {
return "WeekDay2{" +
"name='" + name + '\'' +
", id=" + id +
'}';
}
}
使用enum关键字实现枚举
语法
枚举类型的定义使用关键字enum,语法如下:
枚举的定义语法
[访问权限修饰符] enum 枚举类型名字{
// 枚举体
}
解释:
枚举类型的访问权限修饰符和class类是一致的。
enum是枚举定义关键字,等同于关键字class。
枚举类型的名字可以看成类名,同样需要大驼峰书写,同样“见名知意”。
枚举体的定义,实际上就是定义一个一个的常量,用"逗号,"隔开。枚举体中的单个常量的名字应该全部大写。并且放在枚举体的最上面
按照上述的规则,我们定义一个包含周一到周日的常量的一个枚举类型:
枚举的定义举例
// 枚举类型,使用关键字enum
enum WeekDay {
// 常量对象
MONDAY("周一", 1),
TUESDAY("周二", 2),
WEDNESDAY("周三", 3),
THURSDAY("周四", 4),
FRIDAY("周五", 5),
SATRUDAY("周六", 6),
SUNDAY("周日", 7);
private String name;
private int id;
// 构造方法私有
private WeekDay(String name, int id) {
this.name = name;
this.id = id;
}
public String getName() {
return name;
}
public int getId() {
return id;
}
@Override
public String toString() {
return "WeekDay{" +
"name='" + name + '\'' +
", id=" + id +
'}';
}
}
注意:
- 常量对象需要写在枚举体的首位
基本使用
我们仍然以上述的星期常量为案例,使用枚举类型来改写一下这个方法:
注:switch当中是可以使用枚举类型的。
使用枚举类型来替代常量
// main方法
public static void main(String[] args){
test(WeekDay.TUESDAY);
test(WeekDay.FRIDAY);
test(WeekDay.SUNDAY);
// ...
}
// 测试方法
public static void test(WeekDay day){
switch (day){
case MONDAY:
System.out.println(day);
break;
case TUESDAY:
break;
// ...
case SUNDAY:
break;
}
}
原理(了解)
枚举类型是一种引用数据类型,那么它和class类有什么关系呢?所谓枚举类型是一个什么类型呢?
这个问题的答案很简单,枚举类型就是一个class类,只不过它比较特殊,编译器在。为了还原编译器对枚举类型做的“特殊操作”,我们需要来协助我们。
关于反编译工具CFR的使用,比较简单,请参考文档:使用CFR进行反编译_Ramsey16k的博客-CSDN博客_cfr反编译
注意:在进行反编译的时候需要加参数
--sugarenums false
比如对WeekDayNum.class这个枚举类型编译后的class文件进行反编译的指令是:
java -jar cfr-0.152.jar WeekDayNum.class --sugarenums false
其中"cfr-0.152.jar"是CFR工具的版本号。
请按照以下步骤进行操作:
定义一个枚举类型,比如:
枚举类型
enum WeekDayNum { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY }
编译以上代码,得到WeekDayNum.class文件,并通过CFR反编译工具就可以得到编译器特殊处理后的代码:
编译器特殊处理后的枚举类型
package com.cskaoyan.javase.test; final class WeekDayNum extends Enum<WeekDayNum> { public static final /* enum */ WeekDayNum MONDAY = new WeekDayNum("MONDAY", 0); public static final /* enum */ WeekDayNum TUESDAY = new WeekDayNum("TUESDAY", 1); public static final /* enum */ WeekDayNum WEDNESDAY = new WeekDayNum("WEDNESDAY", 2); public static final /* enum */ WeekDayNum THURSDAY = new WeekDayNum("THURSDAY", 3); public static final /* enum */ WeekDayNum FRIDAY = new WeekDayNum("FRIDAY", 4); public static final /* enum */ WeekDayNum SATURDAY = new WeekDayNum("SATURDAY", 5); public static final /* enum */ WeekDayNum SUNDAY = new WeekDayNum("SUNDAY", 6); private static final /* synthetic */ WeekDayNum[] $VALUES; public static WeekDayNum[] values() { return (WeekDayNum[])$VALUES.clone(); } public static WeekDayNum valueOf(String name) { return Enum.valueOf(WeekDayNum.class, name); } private WeekDayNum(String string, int n) { super(string, n); } static { $VALUES = new WeekDayNum[]{MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY}; } }
根据以上反编译代码,我们可以得出以下结论(枚举类型的原理):
使用enum关键字定义枚举类型并编译后,编译器会为我们生成一个相关的final类,这个类继承了, 所以枚举类型就不能再继承其他类了, 实现接口可以.
枚举类型当中定义的常量,实际上都是这个枚举类型的“public static final”修饰的全局常量对象。
所以,下列代码就可以写出来了:
枚举类型使用代码
WeekDayNum monday = WeekDayNum.MONDAY; // test方法需要一个枚举类型作为参数,实际上是需要传入该枚举类型的对象。而枚举类型当中定义的常量都是该类型的全局常量对象。 test(WeekDayNum.TUESDAY);
构造方法是private的
实际上编译器还自己创建了一个values[]对象数组来存放所有的常量对象,一个values( )方法,一个valueOf(String name )方法, 返回该字符串对应的常量对象
所以枚举类型就是一个普通类,只不过编译器对使用enum关键字定义的类有特殊处理。
练习
需求:
一个支付场景, 假设某个APP只支持支付宝, 微信, 银联, ApplePay 这四种固定支付方式, 需要记录用户使用的支付方式, 以便于统计用户支付习惯进行数据分析(用户画像)
我们可以使用枚举类型来描述这几种固定支付方式
定义枚举类型enum PayType来进行描述, 根据用户选择的支付方式进行记录
定义4个支付常量
ALIPAY, 支付宝支付
WECHATPAY, 微信支付
BANKPAY, 银联支付
APPLEPAY , 苹果支付
Code
public enum PaymentTypeEnum {
ZFB(1,"zfb","支付宝支付"),
WECHAT(2,"wechat","微信支付"),
BANK(3, "bank", "银联支付");
private int id;
private String code;
private String label;
PaymentTypeEnum(int id, String code, String label) {
this.id = id;
this.code = code;
this.label = label;
}
public int getId() {
return id;
}
public String getCode() {
return code;
}
public String getLabel() {
return label;
}
// 根据id查找支付类型
public static PaymentTypeEnum findById(int id) {
for (PaymentTypeEnum type : PaymentTypeEnum.values()) {
if (type.getId() == id) {
return type;
}
}
return null;
}
}
public class Demo {
public static void main(String[] args) {
// 支付类型测试
// 假设前端给我传过来一个支付类型id为2
int id = 2;
// 需要查找类型id为2的支付方式
PaymentTypeEnum type = PaymentTypeEnum.findById(id);
if (type != null) {
testPayment(type);
}
}
public static void testPayment(PaymentTypeEnum type) {
switch (type) {
case ZFB:
System.out.println("采用支付宝支付,支付id=" + type.getId());
break;
case WECHAT:
System.out.println("采用采用微信支付,支付id=" + type.getId());
break;
case BANK:
System.out.println("采用采用银联信支付,支付id=" + type.getId());
break;
}
}
}
使用枚举的场景
- 表示订单状态(已完成, 已支付, 未支付, 进行中, 已取消......)
- 表示支付方式(微信, 支付宝, 银行卡, 信用卡, ApplyPay , GooglePay, Paypal.....)
- 表示物流状态(已揽收, 运送中, 派件, 已签收......)
- 表示用户等级(普通用户, VIP, SVIP.......)
- ......
Date日期类
在JDK版本的迭代中,Java对的设计,可谓是改了又改,提供了很多个类来表示日期:
在JDK1.0版本时,就在java.util包下提供了 Date 类来表示日期,随着JDK版本迭代,这个类当中的方法大多已过时。但作为Java中基础的表示时间和日期的类,它仍然是常用类之一,非常有学习的必要。
而到了Java 8以后,Java中又提供了新的日期表示,提供了诸如:LocalDate、Calendar等新的日期类。
在绝大多数普通开发场景当中,表示时间,使用基础的Date类足够了,这里我们就来学习一下这个Date类,至于Java8之后提供的新日期类,大家可以根据实际情况,再去选择学习一下。
首先,提出一个问题,作为引子:Date类作为一个表示时间和日期的类,是如何表示时间和日期的呢?
Date类的对象表示一个特定的时刻瞬间,精确到毫秒,更具体一点来说,Date类的对象中是通过存储一个long类型的时间戳进而来存储时间的。
时间戳
时间戳本身是一个比较复杂的概念,而Java语言当中所使用的时间戳主要是指Unix时间戳,它的涵义是:
从格林威治时间(GMT时间,世界时的起点)1970年01月01日00时00分00秒(北京时间是1970年01月01日08时00分00秒)到现在的秒数(毫秒数)。
在Java中存储的时间戳是一个long类型的毫秒数,即Date类当中的成员变量:
private transient long fastTime;
构造方法
我们来看一下Date类目前还能够使用的,没有过时的两个构造方法:
// 该构造函数使用当前日期和时间来创建对象
Date()
// 使用一个时间戳来创建对应时间的日期对象
Date(long date)
该类的构造器源码如下:
public Date() {
this(System.currentTimeMillis());
}
public Date(long date) {
fastTime = date;
}
所以实际上,Date类只有一个构造器,那就是给成员变量赋值的构造器。
无参构造器当中使用了以下代码来获取一个毫秒值:
System.currentTimeMillis()
这个方法是一个本地方法,该方法会根据操作系统时间来获取当前的时间戳。该方法还是挺有用的,大家可以记一下。
成员方法
首先,我们先介绍一下Date类的toString方法,该方法的作用是输出该Date对象所表示的时间,格式是:
星期 月份 天数 hh:mm:ss 时区缩写 年份
中国的时区缩写是CST(China Standard Time)
Date类的成员方法也大多过时了,需要大家了解的只有两个:
用一个时间戳来设置Date对象:
void setTime(long time)
获取当前Date对象的时间戳的毫秒值:
long getTime( )
以上方法大家在使用时,必然会觉得时间戳和现实的时间格式差距比较大,难以转换。这里提供一个时间戳的转换网站:
时间戳(Unix timestamp)转换工具 - 在线工具 (tool.lu)
最后,再啰嗦一点就是:中国处在东八区,格林威治时间1970 年 1 月 1 日 00:00:00是中国的1970 年 1 月 1 日 08:00:00
比如我们使用以下方式设置一个Date对象:
date.setTime(0);
打印这个对象,会得到以下输出结果:
Thu Jan 01 08:00:00 CST 1970
SimpleDateFormat日期格式类
构造方法
SimpleDateFormat构造器
// 以传入的字符串格式进行解析或者格式化日期
public SimpleDateFormat(String pattern)
该构造器的参数pattern用来表示日期字符串的格式,但这个格式不是乱写的,请参考以下格式:
- y:表示年,例如yyyy,表示千年年份
- M:表示月份,例如MM,表示月份(最多12,两位数)
- d:表示月份中的天数,例如dd,表示天数(最多31,两位数)
- H:表示一天中的小时数,例如HH,表示小时数(最多24,两位数)
- m:表示小时中的分钟数,例如mm,表示分钟数(最大59,两位数)
- s:表示分钟里的秒数,例如ss,表示秒数(最大59,两位数)
举一个例子来说,假如你希望按照下列格式表示时间:
2022/10/10 10:10:10(2022年10月10日 十时十分十秒)
这个pattern参数就应该写作:
yyyy/MM/dd HH:mm:ss
成员方法
这里特别要强调的一点是:SimpleDateFormat对象是日期格式对象,它仅仅只是描述时间和日期的格式,并不能代表时间和日期。表示时间和日期我们仍然需要Date类来协助!
在使用日期格式类时,我们仅需要关注下面两个成员方法就足够了:
将Date对象转换成对应日期字符串表示,该方法需要传入一个Date对象,然后返回一个日期字符串String对象。(Date ----> String)
这个过程,一般称呼为“格式化”,使用以下方法:
public final String format(Date date)
将日期表示的字符串转换成对应的Date对象,该方法需要传入一个“按照pattern格式编写的日期字符串”,然后返回一个Date对象。(String ----> Date)
这个过程,一般称呼为“解析”,使用以下方法:
public Date parse(String source)
上述两个方法在使用时都可能抛出异常,使用时按照规范使用即可。