前言
我们知道,在现实世界里,实际上数字是有无穷个的,就比如0和1之间,你说有多少个数字?
无数个!
但是在计算机中,数字的个数其实是有限的,因为计算机有存储空间的限制,所以实际上无论是整数还是浮点数,都是有最大范围的。比如在Java中,整型的最大范围是64位的long型整数。
但是有的小伙伴问了,如果我们使用的整数超过了long型的范围怎么办?此时,我们可以通过软件来模拟一个大整数或大浮点数。
在Java中提供了两个用于大数字运算的类,分别是java.math.BigInteger
类 和java.math.BigDecimal
类。
这两个类都可以用于高精度计算,BigInteger类是针对整型大数字的处理类,而BigDecimal类是针对大小数的处理类,我们可以用它们来表示任意大小的整数和浮点数。接下来我们就带大家来学习一下,在Java中如何处理大数字。
全文大约 【3800】字, 不说废话,只讲可以让你学到技术、明白原理的纯干货!本文带有丰富的案例及配图,让你更好地理解和运用文中的技术概念,并可以给你带来具有足够启迪的思考......
一. BigInteger类
1. 简介
在之前给大家讲解8种基本类型时就说过,不同的数据类型,有不同的取值范围,我们再通过下表回顾一下:
类型 |
所占字节(byte) |
所占位数(bit) |
取值范围 |
byte |
1 |
8 |
-2^7 ~ 2^7-1 |
short |
2 |
16 |
-2^15 ~2^15-1 |
int |
4 |
32 |
-2^31 ~ 2^31-1 |
long |
8 |
64 |
-2^63 ~ 2^63-1 |
char |
2字节 |
16位 |
0~65535 |
float |
4字节 |
32位 |
±3.4E+38 |
double |
8字节 |
64位 |
±1.7E+308 |
boolean |
4字节 |
32位 |
true\\false |
从上表中我们可以看到,整型的最大取值范围是-2^63 ~ 2^63-1
,浮点型的最大取值范围是±1.7E+308
。但是不管这个范围有多大,有些小伙伴就想杠一下,如果我就要存一个比Integer或Long更大的数字,怎么办?
针对这种大整数的需求,我们可以使用BigInteger, 它的数字范围比 Integer类型的数字范围要大得多,而且BigInteger支持任意精度的整数。
也就是说在运算中,BigInteger类型可以准确地表示任何大小的整数值。BigInteger和Integer、Long一样都是Number的子类,属于不可变类。 它自身带有一些可以进行运算的方法,包括基本的加、减、乘、除操作,还有很多较为高级的操作,像求绝对值、相反数、最大公约数及判断是否为质数等,所以BigInteger用起来是比较方便的。
2. 使用方法
2.1 常用API方法
如果我们要使用BigInteger类,首先要创建一个BigInteger对象。
BigInteger类提供了多个构造方法,其中最直接的一个是以字符串作为参数的构造方法,即BigInteger(String val)。在创建BigInteger对象之后,我们就可以调用BigInteger类提供的方法,进行各种数学运算了,这些常用的API方法如下:
方法名称 |
说明 |
add(BigInteger val) |
做加法运算 |
subtract(BigInteger val) |
做减法运算 |
multiply(BigInteger val) |
做乘法运算 |
divide(BigInteger val) |
做除法运算 |
remainder(BigInteger val) |
做取余数运算 |
divideAndRemainder(BigInteger val) |
做除法运算,返回数组的第一个值为商,第二个值为余数 |
pow(int exponent) |
做参数的 exponent 次方运算 |
negate() |
取相反数 |
shiftLeft(int n) |
将数字左移 n 位,如果 n 为负数,则做右移操作 |
shiftRight(int n) |
将数字右移 n 位,如果 n 为负数,则做左移操作 |
and(BigInteger val) |
做与运算 |
or(BigInteger val) |
做或运算 |
compareTo(BigInteger val) |
做数字的比较运算 |
equals(Object obj) |
当参数 obj 是 Biglnteger 类型的数字并且数值相等时返回 true, 其他返回 false |
min(BigInteger val) |
返回较小的数值 |
max(BigInteger val) |
返回较大的数值 |
2.2 基本案例
我们先来通过一个案例,来验证一下BigInteger中的数字到底有多大。
public static void main(String[] args)
//创建一个BigInteger对象
BigInteger bi = new BigInteger("1234567890");
//计算1234567890的15次方,
//结果=23589821655914838120947036369147203948318169938519404175968425823418008249115809912616071588527110255905622789563711716349000000000000000
System.out.println(bi.pow(15));
我们会发现,BigInteger可以表示一个非常大的数字,比Integer、Long的范围都要大。
2.3 类型转换
在上面说过,BigInteger其实是Number的子类,我们知道,Number中定义了几个负责类型转换的方法,比如:
● 转换为byte:byteValue()
● 转换为short:shortValue()
● 转换为int:intValue()
● 转换为long:longValue()
● 转换为float:floatValue()
● 转换为double:doubleValue()
我们利用上述几个方法,就可以把BigInteger转换成基本类型。
但是大家要注意,如果BigInteger表示的范围超过了基本类型的范围,在转换时会丢失高位信息,也就是说,结果不一定准确。 所以如果我们需要准确地转换成基本类型,可以使用intValueExact()、longValueExact()这样的方法。不过这种方法在转换时如果超出了基本类型的范围,会直接抛出ArithmeticException异常。我们来验证一下吧。
public static void main(String[] args)
//BigInteger转基本类型
BigInteger bi02 = new BigInteger("123456789000");
//123456789000
System.out.println("转为int类型="+bi02.intValue());
System.out.println("转为float类型="+bi02.floatValue());
System.out.println("转为long类型="+bi02.longValue());
//将123456789000乘以123456789000,然后将结果转为long类型
//java.lang.ArithmeticException: BigInteger out of long range
System.out.println("得到精确结果="+bi02.multiply(bi02).longValueExact());
但是如果BigInteger的值超过了float的最大范围(3.4x1038),结果并不会出现ArithmeticException异常,而是会出现Infinity,如下所示:
//计算999999的99次方,并得到该结果的float值
BigInteger bi03 = new BigInteger("999999").pow(99);
float f = bi03.floatValue();
System.out.println("结果="+f);
2.4 其他用法
接下来我们再来看看其他的API方法都有哪些作用。
import java.math.BigInteger;
import java.util.Scanner;
public class Demo10
public static void main(String[] args)
Scanner scanner = new Scanner(System.in);
System.out.println("请输入一个整数:");
// 保存用户输入的数字
int num = scanner.nextInt();
// 使用输入的数字创建BigInteger对象
BigInteger bi = new BigInteger(num + "");
// 计算大数字加上99的结果
System.out.println("加法的结果:" + bi.add(new BigInteger("99")));
// 计算大数字减去25的结果
System.out.println("减法的结果:" + bi.subtract(new BigInteger("25")));
// 计算大数字乘以3的结果
System.out.println("乘法的结果:" + bi.multiply(new BigInteger("3")));
// 计算大数字除以2的结果
System.out.println("除法的结果:" + bi.divide(new BigInteger("2")));
// 计算大数字除以3的商
System.out.println("取商的结果:" + bi.divideAndRemainder(new BigInteger("3"))[0]);
// 计算大数字除以3的余数
System.out.println("取余的结果:" + bi.divideAndRemainder(new BigInteger("3"))[1]);
// 计算大数字的4次方
System.out.println("4次方的结果:" + bi.pow(4));
// 计算大数字的相反数
System.out.println("取反的结果:" + bi.negate());
在上述案例中,我们将用户输入的数字作为 BigInteger 对象的参数,然后调用该对象的各种方法,实现了加、减、乘、除等运算,并输出了最终的结果。
二. BigDecimal类
1. 简介
虽然都是用于大数字运算的类,但BigDecimal加入了小数的概念,所以是可以操作小数的。而float 和 double类型,只能用来进行科学计算或工程计算,并不适用于精度要求较高的商业计算(如货币计算),所以要用到支持任何精度的BigDecimal类。该类中提供了一系列对应的方法,可以用来做超大浮点数的运算,像加、减、乘和除等。在所有运算中,除法运算是最复杂的,因为存在除不尽的情况,需要我们考虑末位小数的处理方式。
2. 使用方法
2.1 常用构造方法
以下是BigDecimal类的常用构造方法:
● BigDecimal(double val):实例化对象时可以将双精度型转换为BigDecimal类型;
● BigDecimal(String val):实例化对象时可以将字符串形式转换为BigDecimal类型。
2.2 常用API方法
除了构造方法之外,BigDecimal还提供了一些常用的API方法供我们进行数学运算。这些方法与BigInteger的方法类型,很多方法名称和用法也都与之一致,所以这里壹哥就不再一一列出了,接下来我就直接通过一个案例给大家演示这些方法如何使用。
import java.math.BigDecimal;
public class Demo11
public static void main(String[] args)
BigDecimal bd = new BigDecimal("1000.05800");
// 计算大数字加上99的结果
System.out.println("加法的结果:" + bd.add(new BigDecimal("99")));
// 计算大数字减去25的结果
System.out.println("减法的结果:" + bd.subtract(new BigDecimal("25")));
// 计算大数字乘以1000的结果
System.out.println("乘法的结果:" + bd.multiply(new BigDecimal(1000)));
//获取小数的位数,5
System.out.println(bd.scale());
//去掉BigDecimal末尾的0,返回一个与原有BigDecimal相等的新对象
BigDecimal bd2 = bd.stripTrailingZeros();
System.out.println(bd2.scale());
在上述代码中,stripTrailingZeros()方法用于去掉BigDecimal末尾的0,并返回一个与原有BigDecimal相等的新对象。而scale()方法用于获取一个数字后面0的个数,如果返回的是负数,比如-2,则表示该数是一个整数,且末尾有2个0。
2.3 divide()除法
BigDecimal进行加、减、乘时,数字的精度不会丢失,但是进行除法运算时,有可能会出现无法除尽的情况,此时必须指定精度以及如何进行截断。BigDecimal给我们提供了divide()和divideAndRemainder()两个方法可以进行除法运算。
其中,divide()方法有3个参数分别表示除数、商的小数点后的位数和近似值的处理模式,下表是给大家列出的roundingMode参数支持的处理模式。
模式名称 |
说明 |
BigDecimal.ROUND_UP |
商的最后一位,如果大于 0,则向前进位,正负数都如此。 |
BigDecimal.ROUND_DOWN |
商的最后一位无论是什么数字都省略 |
BigDecimal.ROUND_CEILING |
商如果是正数,按照 ROUND_UP 模式处理;如果是负数,按照 ROUND_DOWN模式处理 |
BigDecimal.ROUND_FLOOR |
与 ROUND_CELING 模式相反,商如果是正数,按照 ROUND_DOWN 模式处理;如果是负数,按照 ROUND_UP 模式处理 |
BigDecimal.ROUND_HALF_ DOWN |
对商进行五舍六入操作。如果商最后一位小于等于 5,则做舍弃操作,否则对最后一位进行进位操作 |
BigDecimal.ROUND_HALF_UP |
对商进行四舍五入操作。如果商最后一位小于 5,则做舍弃操作,否则对最后一位进行进位操作 |
BigDecimal.ROUND_HALF_EVEN |
如果商的倒数第二位是奇数,则按照 ROUND_HALF_UP 处理;如果是偶数,则按照 ROUND_HALF_DOWN 处理 |
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* @author 一一哥Sun
*/
public class Demo12
public static void main(String[] args)
BigDecimal d1 = new BigDecimal("123.456");
BigDecimal d2 = new BigDecimal("123.456789");
// 会产生ArithmeticException异常,因为除不尽,可以设置RoundingMode,按照指定的方法进行四舍五入或者直接截断:
//BigDecimal d3 = d1.divide(d2);
// 保留10位小数并四舍五入
BigDecimal d4 = d1.divide(d2, 10, RoundingMode.HALF_UP);
System.out.println("d4="+d4);
//按指定的位数直接截断,0.xxxx
BigDecimal d5 = d1.divide(d2, 4, RoundingMode.DOWN);
System.out.println("d5="+d5);
2.4 divideAndRemainder()除法
而divideAndRemainder()方法,会返回一个数组,内部包含两个BigDecimal,分别是商和余数,其中商总是整数,余数不会大于除数,所以我们可以利用这个方法来判断两个BigDecimal是否是整数倍数。
import java.math.BigDecimal;
import java.math.RoundingMode;
public class Demo12
public static void main(String[] args)
//divideAndRemainder方法,返回一个数组,该数组内部包含了两个BigDecimal,分别是商和余数,其中商总是整数,余数不会大于除数。
//我们可以利用这个特性来判断两个BigDecimal是否是整数倍数。
BigDecimal n = new BigDecimal("123.456");
BigDecimal m = new BigDecimal("0.123");
BigDecimal[] dr = n.divideAndRemainder(m);
System.out.println(dr[0]); // 1003
System.out.println(dr[1]); // 0.087
if (dr[1].signum() == 0)
// n是m的整数倍
System.out.println("n是m的整数倍");
else
System.out.println("n不是m的整数倍");
3. 比较两个BigDecimal
如果我们想比较两个BigDecimal的值是否相等,需要特别注意,请不要使用equals()方法,因为使用该方式进行比较时,不但要求两个BigDecimal的值相等,还要求它们的scale()结果也相等。所以一般是建议使用compareTo()方法来比较,它会根据两个值的大小分别返回负数、正数和0,分别表示小于、大于和等于。如下所示:
import java.math.BigDecimal;
/**
* @author 一一哥Sun
*/
public class Demo13
public static void main(String[] args)
BigDecimal d1 = new BigDecimal("123.456");
BigDecimal d2 = new BigDecimal("123.456000");
// false,因为scale不同
System.out.println("d1==d2? "+d1.equals(d2));
// true,因为d2去除尾部0后scale变为2
System.out.println("d1==d2? "+d1.equals(d2.stripTrailingZeros()));
//结果=0,负数表示小于,正数表示大于,0表示等于
System.out.println("d1==d2? "+d1.compareTo(d2));
之所以需要使用compareTo()方法来比较两个BigDecimal的值才准确,这是因为一个BigDecimal实际上由一个BigInteger和一个scale组合而成的,其中BigInteger表示一个完整的整数,scale表示小数位数。如下图所示:
compareTo()方法内部会对小数位数进行判断,所以更准确,如下图:
三. 结语
至此,我们就把BigInteger、BigDecimal
等大数字类介绍完毕了,最后给大家总结一下今天的重点内容:
● BigInteger用于表示任意大小的整数;
● BigInteger是不变类,并且继承自Number;
● 将BigInteger转换成基本类型时可使用longValueExact()等方法保证结果准确;
● BigDecimal用于表示精确的小数,常用于财务计算;
● 比较BigDecimal的值是否相等,必须使用compareTo()而不能使用equals()。
转载:https://segmentfault.com/a/1190000004597758
本文主要讲述Java类的加载机制,主要包括类加载器、加载过程、初始化时机。
一、类加载器
1、ClassLoader抽象类
类加载器的任务就是根据一个类的全限定名来读取此类的二进制字节流到JVM内部,然后转换为一个与目标类对应的java.lang.Class对象实例。
如果需要支持类的动态加载或需要对编译后的字节码文件进行解密操作等,就需要与类加载器打交道了。
-
BootstrapClassLoader,由C++编写嵌套在JVM内部,负责加载“JAVA_HOME/lib”目录中的所有类型,或者由“-Xbootclasspath”指定路径中的所有类型。
-
ExtClassLoader和AppClassLoader都继承至ClassLoader抽象类,由Java编写。
-
ExtClassLoader负责加载“JAVA_HOME/lib/ext”目录下的所有类型。
-
AppClassLoader负责加载ClassPath目录中的所有类型。
defineClass方法将字节码的byte数组转换为一个类的Class对象实例,如果希望在类被加载到JVM内部时就被链接,那么可以调用resolveClass方法。
2、双亲委派模型
Parents Delegation Model,双亲委派模型,约定类加载器的加载机制。
当一个类加载器接收到一个类加载的任务时,不会立即展开加载,而是将加载任务委派给它的超类加载器去执行,每一层的类都采用相同的方式,直至委派给最顶层的启动类加载器为止。如果超类加载器无法加载委派给它的类,便将类的加载任务退回给下一级类加载器去执行加载。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
使用这种方式的好处是:能够有效确保一个类的全局唯一性,当程序中出现多个全限定名相同的类时,类加载器在执行加载时,始终只会加载其中的某一个类。
使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的Class-Path中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。如果自己去编写一个与rt.jar类库中已有类重名的Java类,将会发现可以正常编译,但永远无法被加载运行。
双亲委派模型对于保证Java程序的稳定运作很重要,但它的实现却非常简单,实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中,逻辑清晰易懂:先检查是否已经被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。
双亲委派机制只是Java虚拟机规范建议采用的加载机制,实际在tomcat中,类加载器所采用的加载机制与传统的双亲委派模型有一定的区别,当缺省的类加载器接收到一个类的加载任务时,首先会去由它自行加载,当它加载失败时,才会将类的加载任务委派给它的超类加载器去执行。
3、自定义类加载器
程序中如果没有显式指定类加载器的话,默认是AppClassLoader来加载,它负责加载ClassPath目录中的所有类型,如果被加载的类型并没有在ClassPath目录中时,抛出java.lang.ClassNotFoundException异常。
一般是继承ClassLoader,如果要符合双亲委派规范,则重写findClass方法;要破坏的话,重写loadClass方法。
双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前——即JDK 1.2发布之前。由于双亲委派模型在JDK 1.2之后才被引入,而类加载器和抽象类java.lang.ClassLoader则在JDK1.0时代就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。
为了向前兼容,JDK 1.2之后的java.lang.ClassLoader添加了一个新的protected方法findClass(),在此之前,用户去继承java.lang.ClassLoader的唯一目的就是为了重写loadClass()方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法的唯一逻辑就是去调用自己的load-Class()。
上一节我们已经看过loadClass()方法的代码,双亲委派的具体逻辑就实现在这个方法之中,JDK1.2之后已不提倡用户再去覆盖loadClass()方法,而应当把自己的类加载逻辑写到findClass()方法中,在loadClass()方法的逻辑里如果父类加载失败,则会调用自己的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派规则的。
二、类加载过程
一个完整的类加载过程必须经历加载、连接、初始化这三个步骤:
1、加载
简单的说,类加载阶段就是由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储在运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.Class对象实例(Java虚拟机规范并没有明确要求一定要存储在堆区中,只是hotspot选择将Class对象存储在方法区中),这个Class对象在日后就会作为方法区中该类的各种数据的访问入口。
2、连接
连接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中,经由验证、准备、解析三个阶段。
(1)验证阶段
验证类数据信息是否符合JVM规范,是否是一个有效的字节码文件,验证内容涵盖了类数据信息的格式验证、语义分析、操作验证等
格式验证:验证是否符合class文件规范,比如以0xCAFEBABE开头,大小版本号等
语义验证:
a、检查一个被标记为final的类型是否包含派生类
b、检查一个类中的final方法是否被派生类进行重写
c、确保超类与派生类之间没有不兼容的一些方法声明(比如方法签名相同,但方法的返回值不同)
操作验证:
在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证(通常在解析阶段执行,检查是否能通过符号引用中描述的全限定名定位到指定类型上,以及类成员信息的访问修饰符是否允许访问等)。
(2)准备阶段
为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量将不再此操作范围内)
(3)解析阶段
将常量池中所有的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法)。这个阶段可以在初始化之后再执行。
3、初始化
将一个类中所有被static关键字标识的代码统一执行一遍,如果执行的是静态变量,那么就会使用用户指定的值覆盖之前在准备阶段设置的初始值;如果执行的是static代码块,那么在初始化阶段,JVM就会执行static代码块中定义的所有操作。
所有类变量初始化语句和静态代码块都会在编译时被前端编译器放在收集器里头,存放到一个特殊的方法中,这个方法就是<clinit>方法,即类/接口初始化方法。该方法的作用就是初始化一个类中的变量,使用用户指定的值覆盖之前在准备阶段设定的初始值。任何invoke之类的字节码都无法调用<clinit>方法,因为该方法只能在类加载的过程中由JVM调用。
如果超类还没有被初始化,那么优先对超类初始化,但在<clinit>方法内部不会显示调用超类的<clinit>方法,由JVM负责保证一个类的<clinit>方法执行之前,它的超类<clinit>方法已经被执行。
JVM必须确保一个类在初始化的过程中,如果是多线程需要同时初始化它,仅仅只能允许其中一个线程对其执行初始化操作,其余线程必须等待,只有在活动线程执行完对类的初始化操作之后,才会通知正在等待的其他线程。
只有那些需要执行java代码来为类变量执行赋值操作的类型在编译之后才会在字节码中存在生成的<clinit>方法。如果一个类并没有声明任何的类变量,也没有静态代码块,那么这个类在编译为字节码后,字节码文件中将不会包含<clinit>方法;同样如果一个类声明类变量,但没有明确使用类变量的初始化语句以及静态代码块来执行初始化操作,编译后的字节码中也不会有<clinit>方法;只有final的静态变量也不会有该方法。
类初始化的6种时机
(1)为一个类型创建一个新的对象实例时(比如new、反射、序列化)
(2)调用一个类型的静态方法时(即在字节码中执行invokestatic指令)
(3)调用一个类型或接口的静态字段,或者对这些静态字段执行赋值操作时(即在字节码中,执行getstatic或者putstatic指令),不过用final修饰的静态字段除外,它被初始化为一个编译时常量表达式
(4)调用JavaAPI中的反射方法时(比如调用java.lang.Class中的方法,或者java.lang.reflect包中其他类的方法)
(5)初始化一个类的派生类时(Java虚拟机规范明确要求初始化一个类时,它的超类必须提前完成初始化操作,接口例外)
(6)JVM启动包含main方法的启动类时。
数组本身并不是由类加载器负责创建,而是由JVM在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建。