28 通过无符号转换转换数字
这个问题要求我们通过无符号转换将给定的有符号的int
转换成long
。那么,让我们考虑签名的Integer.MIN_VALUE
,即 -2147483648。
在 JDK8 中,使用Integer.toUnsignedLong()
方法,转换如下(结果为 2147483648):
long result = Integer.toUnsignedLong(Integer.MIN_VALUE);
下面是另一个将有符号的Short.MIN_VALUE
和Short.MAX_VALUE
转换为无符号整数的示例:
int result1 = Short.toUnsignedInt(Short.MIN_VALUE); int result2 = Short.toUnsignedInt(Short.MAX_VALUE);
其他同类方法有Integer.toUnsignedString()、Long.toUnsignedString()、Byte.toUnsignedInt()、Byte.toUnsignedLong()、Short.toUnsignedInt()、Short.toUnsignedLong()。
29 比较两个无符号数
让我们考虑两个有符号整数,Integer.MIN_VALUE
(-2147483648)和Integer.MAX_VALUE
(2147483647)。比较这些整数(有符号值)将导致 -2147483648 小于 2147483647:
// resultSigned is equal to -1 indicating that // MIN_VALUE is smaller than MAX_VALUE int resultSigned = Integer.compare(Integer.MIN_VALUE, Integer.MAX_VALUE);
在 JDK8 中,这两个整数可以通过Integer.compareUnsigned()方法作为无符号值进行比较(这相当于无符号值的Integer.compare())。该方法主要忽略了符号位的概念,最左边的被认为是最重要的位。在无符号值保护伞下,如果比较的数字相等,则此方法返回 0;如果第一个无符号值小于第二个无符号值,则此方法返回小于 0 的值;如果第一个无符号值大于第二个无符号值,则此方法返回大于 0 的值。
下面的比较返回 1,表示Integer.MIN_VALUE的无符号值大于Integer.MAX_VALUE的无符号值:
// resultSigned is equal to 1 indicating that // MIN_VALUE is greater than MAX_VALUE int resultUnsigned = Integer.compareUnsigned(Integer.MIN_VALUE, Integer.MAX_VALUE);
compareUnsigned()
方法在以 JDK8 开始的Integer
和Long
类中可用,在以 JDK9 开始的Byte
和Short
类中可用。
30 无符号值的除法和模
JDK8 无符号算术 API 通过divideUnsigned()
和remainderUnsigned()
方法支持计算两个无符号值的除法所得的无符号商和余数。
让我们考虑一下Interger.MIN_VALUE
和Integer.MAX_VALUE
有符号数,然后应用除法和模。这里没有什么新鲜事:
// signed division // -1 int divisionSignedMinMax = Integer.MIN_VALUE / Integer.MAX_VALUE; // 0 int divisionSignedMaxMin = Integer.MAX_VALUE / Integer.MIN_VALUE; // signed modulo // -1 int moduloSignedMinMax = Integer.MIN_VALUE % Integer.MAX_VALUE; // 2147483647 int moduloSignedMaxMin = Integer.MAX_VALUE % Integer.MIN_VALUE;
现在,我们将Integer.MIN_VALUE
和Integer.MAX_VALUE
视为无符号值,并应用divideUnsigned()
和remainderUnsigned()
:
// division unsigned int divisionUnsignedMinMax = Integer.divideUnsigned( Integer.MIN_VALUE, Integer.MAX_VALUE); // 1 int divisionUnsignedMaxMin = Integer.divideUnsigned( Integer.MAX_VALUE, Integer.MIN_VALUE); // 0 // modulo unsigned int moduloUnsignedMinMax = Integer.remainderUnsigned( Integer.MIN_VALUE, Integer.MAX_VALUE); // 1 int moduloUnsignedMaxMin = Integer.remainderUnsigned( Integer.MAX_VALUE, Integer.MIN_VALUE); // 2147483647
注意它们与比较操作的相似性。这两种操作,即无符号除法和无符号模运算,都将所有位解释为值位,并忽略符号位。
divideUnsigned() and remainderUnsigned() are present in the Integer and Long classes, respectively.
31 double/float
是一个有限的浮点值
这个问题产生于这样一个事实:一些浮点方法和操作产生Infinity
或NaN
作为结果,而不是抛出异常。
检查给定的float/double是否为有限浮点值的解决方案取决于以下条件:给定的float/double值的绝对值不得超过float/double类型的最大正有限值:
// for float Math.abs(f) <= Float.MAX_VALUE; // for double Math.abs(d) <= Double.MAX_VALUE
从 Java8 开始,前面的条件通过两个专用的标志方法Float.isFinite()
和Double.isFinite()
公开。因此,以下示例是有限浮点值的有效测试用例:
Float f1 = 4.5f; boolean f1f = Float.isFinite(f1); // f1 = 4.5, is finite Float f2 = f1 / 0; boolean f2f = Float.isFinite(f2); // f2 = Infinity, is not finite Float f3 = 0f / 0f; boolean f3f = Float.isFinite(f3); // f3 = NaN, is not finite Double d1 = 0.000333411333d; boolean d1f = Double.isFinite(d1); // d1 = 3.33411333E-4,is finite Double d2 = d1 / 0; boolean d2f = Double.isFinite(d2); // d2 = Infinity, is not finite Double d3 = Double.POSITIVE_INFINITY * 0; boolean d3f = Double.isFinite(d3); // d3 = NaN, is not finite
这些方法在以下情况下非常方便:
if (Float.isFinite(d1)) { // do a computation with d1 finite floating-point value } else { // d1 cannot enter in further computations }
32 对两个布尔表达式应用逻辑与/或/异或
基本逻辑运算的真值表(与、或、异或)如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OectRBUa-1657077108927)(https://github.com/apachecn/apachecn-java-zh/raw/master/docs/java-coding-prob/img/482c6d19-72f9-41a3-9590-52cde694d0a0.png)]
在 Java 中,逻辑和运算符表示为&&,逻辑或运算符表示为||,逻辑异或运算符表示为^。从 JDK8 开始,这些运算符被应用于两个布尔值,并被包装在三个static方法中—Boolean.logicalAnd()、Boolean.logicalOr()和Boolean.logicalXor():
int s = 10; int m = 21; // if (s > m && m < 50) { } else { } if (Boolean.logicalAnd(s > m, m < 50)) {} else {} // if (s > m || m < 50) { } else { } if (Boolean.logicalOr(s > m, m < 50)) {} else {} // if (s > m ^ m < 50) { } else { } if (Boolean.logicalXor(s > m, m < 50)) {} else {}
也可以结合使用这些方法:
if (Boolean.logicalAnd( Boolean.logicalOr(s > m, m < 50), Boolean.logicalOr(s <= m, m > 50))) {} else {}
33 将BigInteger
转换为原始类型
BigInteger
类是表示不可变的任意精度整数的非常方便的工具。
此类还包含用于将BigInteger
转换为byte
、long
或double
等原始类型的方法(源于java.lang.Number
)。然而,这些方法会产生意想不到的结果和混乱。例如,假设有BigInteger
包裹Long.MAX_VALUE
:
BigInteger nr = BigInteger.valueOf(Long.MAX_VALUE);
让我们通过BigInteger.longValue()
方法将这个BigInteger
转换成一个原始long
:
long nrLong = nr.longValue();
到目前为止,由于Long.MAX_VALUE
是 9223372036854775807,nrLong
原始类型变量正好有这个值,所以一切都按预期工作。
现在,让我们尝试通过BigInteger.intValue()
方法将这个BigInteger
类转换成一个原始的int
值:
int nrInt = nr.intValue();
此时,nrInt原始类型变量的值将为 -1(相同的结果将产生shortValue()和byteValue()。根据文档,如果BigInteger的值太大,无法容纳指定的原始类型,则只返回低位n位(n取决于指定的原始类型)。
但是如果代码没有意识到这个语句,那么它将在进一步的计算中把值推为 -1,这将导致混淆。
但是,从 JDK8 开始,添加了一组新的方法。这些方法专门用于识别从BigInteger转换为指定的原始类型过程中丢失的信息。如果检测到丢失的信息,则抛出ArithmeticException。这样,代码表示转换遇到了一些问题,并防止了这种不愉快的情况。
这些方法是longValueExact()、intValueExact()、shortValueExact()、byteValueExact():
long nrExactLong = nr.longValueExact(); // works as expected int nrExactInt = nr.intValueExact(); // throws ArithmeticException
注意,intValueExact()
没有返回 -1 作为intValue()
。这一次,由于试图将最大的long
值转换为int
而导致的信息丢失通过ArithmeticException
类型的异常发出信号。
34 将long
转换为int
将long
值转换为int
值似乎是一件容易的工作。例如,潜在的解决方案可以依赖于以下条件:
long nr = Integer.MAX_VALUE; int intNrCast = (int) nr;
或者,它可以依赖于Long.intValue()
,如下所示:
int intNrValue = Long.valueOf(nrLong).intValue();
两种方法都很有效。现在,假设我们有以下long
值:
long nrMaxLong = Long.MAX_VALUE;
这一次,两种方法都将返回 -1。为了避免这种结果,建议使用 JDK8,即Math.toIntExact()。此方法获取一个long类型的参数,并尝试将其转换为int。如果得到的值溢出了int,则该方法抛出
ArithmeticException: // throws ArithmeticException int intNrMaxExact = Math.toIntExact(nrMaxLong);
在幕后,toIntExact()
依赖于((int)value != value)
条件。
35 除法的下限与模的计算
假设我们有以下划分:
double z = (double)222/14;
这将用这个除法的结果初始化z
,即 15.85,但是我们的问题要求这个除法的下限是 15(这是小于或等于代数商的最大整数值)。获得该期望结果的解决方案将包括应用Math.floor(15.85)
,即 15。
但是,222 和 14 是整数,因此前面的除法如下:
int z = 222/14;
这一次,z将等于 15,这正是预期的结果(/运算符返回最接近零的整数)。无需申请Math.floor(z)。此外,如果除数为 0,则222/0将抛出ArithmeticException。
到目前为止的结论是,两个符号相同的整数(都是正的或负的)的除法底可以通过/运算符得到。
好的,到目前为止,很好,但是假设我们有以下两个整数(相反的符号;被除数是负数,除数是正数,反之亦然):
double z = (double) -222/14;
此时,z
将等于 -15.85。同样,通过应用Math.floor(z)
,结果将是 -16,这是正确的(这是小于或等于代数商的最大整数值)。
让我们用int
再次讨论同样的问题:
int z = -222/14;
这次,z将等于 -15。这是不正确的,Math.floor(z)在这种情况下对我们没有帮助,因为Math.floor(-15)是 -15。所以,这是一个应该考虑的问题。
从 JDK8 开始,所有这些病例都通过Math.floorDiv()方法被覆盖和暴露。此方法以表示被除数和除数的两个整数为参数,返回小于或等于代数商的最大值(最接近正无穷大)int值:
int x = -222; int y = 14; // x is the dividend, y is the divisor int z = Math.floorDiv(x, y); // -16
Math.floorDiv()方法有三种口味:floorDiv(int x, int y)、floorDiv(long x, int y)和floorDiv(long x, long y)。
在Math.floorDiv()之后,JDK8 附带了Math.floorMod(),它返回给定参数的地板模量。这是作为x - (floorDiv(x, y) * y)的结果计算的,因此对于符号相同的参数,它将返回与%运算符相同的结果;对于符号不相同的参数,它将返回不同的结果。
将两个正整数(a/b)的除法结果四舍五入可以快速完成,如下所示:
long result = (a + b - 1) / b;
下面是一个例子(我们有4/3=1.33
,我们想要 2):
long result = (4 + 3 - 1) / 3; // 2
下面是另一个例子(我们有17/7=2.42
,我们想要 3):
long result = (17 + 7 - 1) / 7; // 3
如果整数不是正的,那么我们可以依赖于Math.ceil()
:
long result = (long) Math.ceil((double) a/b);
36 下一个浮点值
有一个整数值,比如 10,使得我们很容易得到下一个整数值,比如10+1
(在正无穷方向)或者10-1
(在负无穷方向)。尝试为float
或double
实现同样的目标并不像对整数那么容易。
从 JDK6 开始,Math类通过nextAfter()方法得到了丰富。此方法采用两个参数,即初始数字(float或double)和方向(Float/Double.NEGATIVE/POSITIVE_INFINITY)——并返回下一个浮点值。在这里,返回 0.1 附近的下一个负无穷方向的浮点值是这种方法的一个特色:
float f = 0.1f; // 0.099999994 float nextf = Math.nextAfter(f, Float.NEGATIVE_INFINITY);
从 JDK8 开始,Math
类通过两个方法进行了丰富,这两个方法充当了nextAfter()
的快捷方式,而且速度更快。这些方法是nextDown()
和nextUp()
:
float f = 0.1f; float nextdownf = Math.nextDown(f); // 0.099999994 float nextupf = Math.nextUp(f); // 0.10000001 double d = 0.1d; double nextdownd = Math.nextDown(d); // 0.09999999999999999 double nextupd = Math.nextUp(d); // 0.10000000000000002
因此,在负无穷大方向上的nextAfter()
可通过Math.nextDown()
和nextAfter()
获得,而在正无穷大方向上的Math.nextUp()
可通过Math.nextUp()
获得。
37 将两个大int/long
值相乘并溢出
让我们从*
操作符开始深入研究解决方案,如下例所示:
int x = 10; int y = 5; int z = x * y; // 50
这是一种非常简单的方法,对于大多数涉及int、long、float和double的计算都很好。
现在,让我们将此运算符应用于以下两个大数(将 2147483647 与自身相乘):
int x = Integer.MAX_VALUE; int y = Integer.MAX_VALUE; int z = x * y; // 1
此时,z将等于 1,这不是预期的结果,即 4611686014132420609。仅将z类型从int更改为long将无济于事。但是,将x和y的类型从int改为long将:
long x = Integer.MAX_VALUE; long y = Integer.MAX_VALUE; long z = x * y; // 4611686014132420609
但是如果我们用Long.MAX_VALUE
代替Integer.MAX_VALUE
,问题会再次出现:
long x = Long.MAX_VALUE; long y = Long.MAX_VALUE; long z = x * y; // 1
因此,溢出域并依赖于*运算符的计算将最终导致误导性结果。
与其在进一步的计算中使用这些结果,不如在发生溢出操作时及时得到通知。JDK8 附带了Math.multiplyExact()方法。此方法尝试将两个整数相乘。如果结果溢出,int只抛出ArithmeticException:
int x = Integer.MAX_VALUE; int y = Integer.MAX_VALUE; int z = Math.multiplyExact(x, y); // throw ArithmeticException
在 JDK8 中,Math.muliplyExact(int x, int y)返回int,Math.muliplyExact(long x, long y)返回long。在 JDK9 中,还添加了Math.muliplyExact(long, int y)返回long。
JDK9 带有返回值为long的Math.multiplyFull(int x, int y)。此方法对于获得两个整数的精确数学积long非常有用,如下所示:
int x = Integer.MAX_VALUE; int y = Integer.MAX_VALUE; long z = Math.multiplyFull(x, y); // 4611686014132420609
为了记录在案,JDK9 还附带了一个名为Math.muliptlyHigh(long x, long y)
的方法,返回一个long
。此方法返回的long
值表示两个 64 位因子的 128 位乘积的最高有效 64 位:
long x = Long.MAX_VALUE; long y = Long.MAX_VALUE; // 9223372036854775807 * 9223372036854775807 = 4611686018427387903 long z = Math.multiplyHigh(x, y);
在函数式上下文中,潜在的解决方案将依赖于BinaryOperator
函数式接口,如下所示(只需定义相同类型的两个操作数的操作):
int x = Integer.MAX_VALUE; int y = Integer.MAX_VALUE; BinaryOperator<Integer> operator = Math::multiplyExact; int z = operator.apply(x, y); // throw ArithmeticException
对于处理大数,还应关注BigInteger
(不可变任意精度整数)和BigDecimal
(不可变任意精度带符号十进制数)类。
38 融合乘法加法
数学计算a * b + c
在矩阵乘法中被大量利用,在高性能计算、人工智能应用、机器学习、深度学习、神经网络等领域有着广泛的应用。
实现此计算的最简单方法直接依赖于*
和+
运算符,如下所示:
double x = 49.29d; double y = -28.58d; double z = 33.63d; double q = (x * y) + z;
这种实现的主要问题是由两个舍入误差(一个用于乘法运算,一个用于加法运算)引起的精度和性能低下。
不过,多亏了 Intel AVX 执行 SIMD 操作的指令和 JDK9,JDK9 添加了Math.fma()方法,这种计算才得以提高。依靠Math.fma(),使用“四舍五入到最接近的偶数四舍五入”模式只进行一次四舍五入:
double fma = Math.fma(x, y, z);
请注意,这种改进适用于现代 Intel 处理器,因此仅使用 JDK9 是不够的。
39 紧凑数字格式
从 JDK12 开始,添加了一个用于紧凑数字格式的新类。这个类被命名为java.text.CompactNumberFormat。这个类的主要目标是扩展现有的 Java 数字格式化 API,支持区域设置和压缩。
数字可以格式化为短样式(例如,1000变成1K),也可以格式化为长样式(例如,1000变成1000)。这两种风格在Style枚举中分为SHORT和LONG。
除了CompactNumberFormat构造器外,还可以通过两个static方法创建CompactNumberFormat,这两个方法被添加到NumberFormat类中:
- 第一种是默认语言环境的紧凑数字格式,带有
NumberFormat.Style.SHORT
:
public static NumberFormat getCompactNumberInstance()
第二种是指定区域设置的紧凑数字格式,带有NumberFormat.Style:
public static NumberFormat getCompactNumberInstance( Locale locale, NumberFormat.Style formatStyle)
让我们仔细看看格式化和解析。
格式化
默认情况下,使用RoundingMode.HALF_EVEN
格式化数字。但是,我们可以通过NumberFormat.setRoundingMode()
显式设置舍入模式。
尝试将这些信息压缩成一个名为NumberFormatters
的工具类可以实现如下:
public static String forLocale(Locale locale, double number) { return format(locale, Style.SHORT, null, number); } public static String forLocaleStyle( Locale locale, Style style, double number) { return format(locale, style, null, number); } public static String forLocaleStyleRound( Locale locale, Style style, RoundingMode mode, double number) { return format(locale, style, mode, number); } private static String format( Locale locale, Style style, RoundingMode mode, double number) { if (locale == null || style == null) { return String.valueOf(number); // or use a default format } NumberFormat nf = NumberFormat.getCompactNumberInstance(locale, style); if (mode != null) { nf.setRoundingMode(mode); } return nf.format(number); }
现在,我们将数字1000
、1000000
和1000000000
格式化为US
语言环境、SHORT
样式和默认舍入模式:
// 1K NumberFormatters.forLocaleStyle(Locale.US, Style.SHORT, 1_000); // 1M NumberFormatters.forLocaleStyle(Locale.US, Style.SHORT, 1_000_000);
// 1B NumberFormatters.forLocaleStyle(Locale.US, Style.SHORT, 1_000_000_000);
我们可以对LONG
样式做同样的处理:
// 1thousand NumberFormatters.forLocaleStyle(Locale.US, Style.LONG, 1_000); // 1million NumberFormatters.forLocaleStyle(Locale.US, Style.LONG, 1_000_000); // 1billion NumberFormatters.forLocaleStyle(Locale.US, Style.LONG, 1_000_000_000);
我们也可以使用ITALIAN
区域设置和SHORT
样式:
// 1.000 NumberFormatters.forLocaleStyle(Locale.ITALIAN, Style.SHORT, 1_000); // 1 Mln NumberFormatters.forLocaleStyle(Locale.ITALIAN, Style.SHORT, 1_000_000); // 1 Mld NumberFormatters.forLocaleStyle(Locale.ITALIAN, Style.SHORT, 1_000_000_000);
最后,我们还可以使用ITALIAN
语言环境和LONG
样式:
// 1 mille NumberFormatters.forLocaleStyle(Locale.ITALIAN, Style.LONG, 1_000); // 1 milione NumberFormatters.forLocaleStyle(Locale.ITALIAN, Style.LONG, 1_000_000); // 1 miliardo NumberFormatters.forLocaleStyle(Locale.ITALIAN, Style.LONG, 1_000_000_000);
现在,假设我们有两个数字:1200
和1600
。
从取整方式来看,分别取整为1000
和2000
。默认取整模式HALF_EVEN
将1200
取整为1000
,将1600
取整为2000
。但是如果我们想让1200
变成2000
,1600
变成1000
,那么我们需要明确设置取整模式如下:
// 2000 (2 thousand) NumberFormatters.forLocaleStyleRound( Locale.US, Style.LONG, RoundingMode.UP, 1_200); // 1000 (1 thousand) NumberFormatters.forLocaleStyleRound( Locale.US, Style.LONG, RoundingMode.DOWN, 1_600);
解析
解析是格式化的相反过程。我们有一个给定的字符串,并尝试将其解析为一个数字。这可以通过NumberFormat.parse()
方法来实现。默认情况下,解析不利用分组(例如,不分组时,5,50K
解析为5
;分组时,5,50K
解析为550000
)。
如果我们将此信息压缩为一组助手方法,则获得以下输出:
public static Number parseLocale(Locale locale, String number) throws ParseException { return parse(locale, Style.SHORT, false, number); } public static Number parseLocaleStyle( Locale locale, Style style, String number) throws ParseException { return parse(locale, style, false, number); } public static Number parseLocaleStyleRound( Locale locale, Style style, boolean grouping, String number) throws ParseException { return parse(locale, style, grouping, number); } private static Number parse( Locale locale, Style style, boolean grouping, String number) throws ParseException { if (locale == null || style == null || number == null) { throw new IllegalArgumentException( "Locale/style/number cannot be null"); } NumberFormat nf = NumberFormat.getCompactNumberInstance(locale, style); nf.setGroupingUsed(grouping); return nf.parse(number); }
让我们将5K
和5 thousand
解析为5000
,而不显式分组:
// 5000 NumberFormatters.parseLocaleStyle(Locale.US, Style.SHORT, "5K"); // 5000 NumberFormatters.parseLocaleStyle(Locale.US, Style.LONG, "5 thousand");
现在,我们用显式分组解析5K
和5 thousand
到5000
:
// 550000 NumberFormatters.parseLocaleStyleRound( Locale.US, Style.SHORT, true, "5,50K"); // 550000 NumberFormatters.parseLocaleStyleRound( Locale.US, Style.LONG, true, "5,50 thousand");
通过setCurrency()、setParseIntegerOnly()、setMaximumIntegerDigits()、setMinimumIntegerDigits()、setMinimumFractionDigits()和setMaximumFractionDigits()方法可以获得更多的调优。
总结
本章收集了一系列涉及字符串和数字的最常见问题。显然,这样的问题有很多,试图涵盖所有这些问题远远超出了任何一本书的范围。然而,了解如何解决本章中提出的问题,为您自己解决许多其他相关问题提供了坚实的基础