找回密码
 立即注册
查看: 345|回复: 0

JavaGuide基础知识点详解

[复制链接]
发表于 2022-6-30 15:23 | 显示全部楼层 |阅读模式
为什么java中只有值传递?


说这个问题之前,我们先来搞懂下面两个概念:
    形参&实参值传递&引用传递
形参&实参


方法的定义可能会用到参数(有参的方法),参数在程序语言中分为:
    实参(实际参数):用于传递给函数/方法的参数,必须有确定值。形参(形式参数):用于定义函数/方法,接受实参,不需要有确定的值。
值传递&引用传递


程序设计语言将实参传递给方法(或函数)的方式分为两种:
    值传递:方法接收的是实参的拷贝,会创建副本引用传递:方法接收的直接是实参所引用的对象在堆中的地址,不会创建副本,对形参的修改将直接影响实参。

很多程序设计语言(比如C++,Pascal)提供了两种参数传递的方式,不过在java中只有值传递。
为什么java只有值传递?


案例一:传递基本类型参数
    public static void swap(int a,int b){        int temp = a;        a = b;        b = temp;        System.out.println("交换方法中a=="+a);        System.out.println("交换方法中b=="+b);    }    public static void main(String[] args) {        int a = 1;        int b = 2;        swap(a, b);        System.out.println("调用交换方法后,a=="+a);        System.out.println("调用交换方法后,b=="+b);    }

demo1

如上这段代码,在swap中交换 a,b的值并不影响传入的a,b本身。因为作为参数传入的其实只是外面的a,b的副本。副本的内容无论如何修改都不会影响到原件本身。
案例二:传递引用类型参数1
    public static void main(String[] args) {        Integer[] arr = new Integer[]{1,2,3};        System.out.println(arr[0]);        change(arr);        System.out.println(arr[0]);    }    public static void change(Integer[] arr){        arr[0] = 0;    }

demo2

看了这个案例会让人有错觉,感觉java对引用类型的参数采用了引用传递。实际上并不是,这里传递的还是值,只不过这个值是实参的地址罢了。其实下面这个demo可以很明确的看出传入的是形参:
    public static void main(String[] args) {        Integer[] arr = new Integer[]{1,2,3};        System.out.println(arr[0]);        change(arr);        System.out.println(arr[0]);    }    public static void change(Integer[] arr){        arr = new Integer[]{6};        System.out.println(arr[0]);    }

demo3

形参指向修改后没有影响实参,因为传入的形数只是拷贝了实参的地址。当指向修改后对实参是没有变化的。而demo2中因为地址还是实参的地址,对这个对象进行修改也会影响实参。
总结:java中将实参传递给方法(或函数)的方式是值传递:

    如果参数是基本类型的话,传递的就是基本类型的字面量值的拷贝,会创建副本。如果参数是引用类型,传递的就是实参所引用的对象在堆中地址值的拷贝,同样也会创建副本。
序列化和反序列化相关概念

什么是序列化什么是反序列化?


如果我们需要持久化java对象比如将java对象保存在文件中,数据库中或者在网络传输java对象,这些场景都要用到序列化。
简单来说:
    序列化:将数据结构或者对象转化成二进制字节流的过程。反序列化:将在序列化过程中所生成的二进制字节流转化成数据结构或者对象的过程。

对于java语言来说,我们序列化的都是对象,C++这种半面向对象语言的中,struct定义的是数据结构类型,class是对象类型。
综上,序列化的主要目的是通过网络传输对象或者说将对象存储到文件系统,数据库,内存中。
实际开发中哪些场景用到序列化和反序列化?

    对象在进行网络传输(比如远程方法调用rpc的时候)之前需要先被序列化,接收到序列化的对象之后再进行反序列化。将对象存储到文件中的时候需要进行序列化,将对象从文件中读取出来需要进行反序列化。将对象存储到缓存数据库(如redis)时需要用到序列化,将对象从缓存数据库中读取出来的时候需要反序列化。
序列化协议对应于TCP/IP 四层模型的哪一层?


我们知道网络通信双方必须采用和遵循相同的协议。TPC/IP 四层模型是这样的。
    应用层传输层网络层
    网络接口层


    七层模型


如上图所示,OSI七层协议模型中,表示层做的事情主要就是对应用层的用户数据进行处理转换为二进制流。反过来的话,就是将二进制流转化成应用层的用户数据,这就是对应的序列化和反序列化。
因为OSI七层协议模型中的应用层,表示层和会话层对应的都是TCP/IP四层模型中的应用层,所以序列化协议属于TCP/IP协议应用层的一部分
常见序列化协议对比


JDK自带的序列化方式一般不会用,因为序列化效率低并且部分版本有安全漏洞。比较常用的序列化协议有hessian,kyro,protostuff。
下面都是二进制的序列化协议,像JSON和xml这种属于文本序列化方式。虽然JSON和XML这种可读性比较好,但是性能较差,一般不会选择。

  • JDK自带的序列化方式
    只需要实现java.io.Serializable接口即可。
    实现这个序列化是需要有个serialVersionUID作为序列化号的,这个序列化号也会被写入二进制序列中,当反序列化的时候会检查当前类的序列化号是否一致。不一致会报错。我们一般都会手动指定每个类的序列化号,如果不指定编译器会动态生成默认的序列化号。
    我们很少或者说几乎不会直接使用这个序列化方式,主要原因有两个:
    不支持跨语言调用:如果调用的是其他语言开发的服务时就不支持了。性能差:相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大。



  • Kryo
    Kryo是一个高性能的序列化/反序列化工具,由于其变长存储特性并使用了字节码生成机制,拥有较高的运行速度和较小的字节码体积。
    另外,Kryo已经是一种非常成熟的序列化实现了,在Twitter,Yahoo以及多个注明开源项目中广泛使用。
    guide-rpc-framework就是使用kryo进行序列化和反序列化的。

  • Protobuf
    Protobuf出自于Google,性能还比较优秀,也支持多种语言,同时还是跨平台的,就是在使用中过于繁琐,因为需要自己定义IDL文件和生成对应的序列化代码。这样虽然不灵活,但是导致Protobuf没有序列化漏洞的风险。

  • ProtoStuff
    由于Protobuf的使用繁琐,所以他的哥哥ProtoStuff诞生了。ProtoStuff是基于protobuf,但是提供了更多的功能和更简易的用法,虽然更加易用,但是不代表ProtoStuff性能差。

  • hession
    hession是一个轻量级的,自定义描述的二进制rpc协议,hession是一个比较老的序列化实现了,同样也是跨语言的。


    dubbo默认的序列化方式

    dubbo RPC默认启用的序列化方式是hession2,但是dubbo对hession2进行了修改,不过大体结构还是差不多。
总结


Kryo是专门针对java语言的一种序列化方式,并且性能非常好。如果你的应用专门针对java语言的话可以考虑使用。
像是Protobuf,ProtoStuff,hession这类都是跨语言的序列化方式,如果有跨语言需求可以考虑使用。
反射机制详解

何为反射?


反射之所以被称为框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。
通过反射可以获取任意一个类的所有属性和方法,还可以调用这些方法和属性。
反射的应用场景


业务代码很少接触反射,但是框架中大量使用了反射。
动态代理的实现依赖反射。
注解的实现依赖反射:注解起作用的原理是反射分析类,然后获取到类/属性/方法/方法参数上的注解,然后再做进一步的处理。
反射机制的优缺点

    优点:让代码更灵活,为各种框架提供开箱即用的功能提供了便利。缺点:让我们在运行时有了分析操作类的能力,这样增加了安全问题。另外反射的性能稍微差点。
反射实战


获取Class对象的四种方式
如果我们动态获取到这些信息,需要依靠Class对象,Class类对象将一个类的方法,变量等信息告诉运行的程序,java提供了四种方式获取Class对象:

  • 知道具体类的情况下可以直接使用.class
    但是很多时候我们是不知道具体类的,有时扫描某个包下的所有类,不能一一去初始化。通过Class.forName()传入类的全路径获取通过对象实例instance.getClass()获取通过类加载器xxxClassLoader.loadClass()传入类路径获取

下面是四种方式获取class的代码:
public static void main(String[] args) {        //直接获取某类的class        Class<Person> person1 = Person.class;        try {            //通过类的全路径名获取class            Class person2 = Class.forName("com.in.g.pack.controller.Person");            for(Method method : person2.getMethods()){                System.out.println(method.getName());            }        } catch (ClassNotFoundException e) {            e.printStackTrace();        }        //实例获取class        Class person3 = new Person("cs").getClass();        try {            //类加载器获取class            Class person4 = ClassLoader.getSystemClassLoader().loadClass("com.in.g.pack.controller.Person");            for(Method method : person4.getMethods()){                System.out.println(method.getName());            }        } catch (ClassNotFoundException e) {            e.printStackTrace();        }    }
其实总结一下:类和实例都可以直接获取。然后类的全路径可以指定获取,不过传路径的方式存在这个路径不存在的情况,所以要抛出异常。而且类加载器获取class不会初始化对象,意味着静态代码块和静态对象不会执行。
Java代理模式详解

代理模式


代理模式是一种比较好理解的设计模式。简单来说就是我们使用代理对象来代替真实对象的访问。这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。
代理模式的主要作用是扩展目标对象的功能,比如说在目标对象的某个方法执行前后你可以增加一些自定义的操作。
举个例子:腾讯代理了某款著名国外游戏,然后我们注册登录都是腾讯的平台,但是我们玩的还是国外的那个游戏。在这里我们可以把游戏看做是目标对象,腾讯看做的游戏的代理对象。
代理模式有静态代理和动态代理两种实现方式。
静态代理


静态代理中,我们对目标对象的每个方法的增强都是手动完成的。非常不灵活(比方说接口一旦新增方法,目标对象和代理对象都要进行修改),且麻烦(需要每个目标类都单独写一个代理类),实际应用场景非常少,日常开发几乎看不到使用静态代理的场景。
上面是从实现和应用角度来说的静态代理,从JVM的层面说:静态代理在编译时就将接口,实现类,代理类这些都变成了一个个实际的class文件。
静态代理实现步骤:
    定义一个接口及其实现类。创建一个代理类同样实现这个接口。将目标对象注入代理类,然后在代理类的方法中调用目标类的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己的逻辑。

这个就没必要写demo了,我们可以理解为service被实现了两次,一次是目标类实现写正常业务逻辑,还有一个代理类实现,调用目标类对应的方法,且前后可以加点自己想写的东西。
动态代理


相比于静态代理来说,动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,我们可以直接代理实现类。
从JVM的角度来说,动态代理是在运行时动态生成类字节码,并加载到JVM中。
说到动态代理,spring AOP,RPC框架应该是两个不得不提的,他们的实现都依赖了动态代理。
动态代理在我们日常开发中用的比较少,但是在框架中几乎是必用的一门技术。学会了动态代理后,对于我们理解和学习各种框架的原理非常有帮助。
就java来说,动态代理的实现方式有很多种,比如JDK动态代理,CGLIB动态代理等。
JDK动态代理机制


在java动态代理机制中,InvocationHandler接口和Proxy类是核心。
Proxy类中使用频率最高的方法:newProxyInstance(),这个方法主要用来生成一个代理对象。
这个方法一共有三个参数:
    loader:类加载器,用于加载代理对象interfaces:被代理类实现的一些接口h:实现了InvocationHandler接口的对象

要实现动态代理的话,还必须要实现InvocationHandler来自定义处理逻辑。当我们的动态代理对象调用一个方法时,这个方法的调用就会被转发到实现InvocationHandler接口类的invoke方法来调用。
invoke()方法有三个参数:
    proxy:动态生成的代理类method: 与代理类对象调用的方法相对应args: 当前method方法的参数

也就是说:你通过Proxy类的newProxyInstance()创建的代理对象在调用方法的时候,实际会调用到实现InvocationHandler接口的类的invoke()方法。你可以在invoke()方法中自定义处理逻辑,比如在方法执行前后加什么。

JDK动态代理使用步骤
    定义一个接口以及实现类自定义InvocationHandler并重写invoke方法,在invoke方法中我们会调用原生方法(被代理类的方法)并自定义一些处理逻辑通过Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces.InvocationHandler h)方法创建代理对象。

代码示例

测试代理

这里其实最后一步不应该直接写在main方法中获取对象,应该封装成一个工厂类,但是我这里图方便就直接写了。运行结果我也截图了,和我们预测的一样。
CGLIB动态代理机制


JDK动态代理有一个最致命的问题就是其只能代理实现了接口的类。为了解决这个问题,我们可以用CGLIB动态代理机制来避免。
在CGLIB动态代理机制中,MethodInterceptor接口和Enhancer类是核心。
我们需要自定义MethodInterceptor并重写Intercept放啊,Intercept用于拦截被代理类的方法。
CGLIB动态代理类使用步骤
    定义一个类。自定义MethodInterceptor并重写Intercept方法,Intercept用于拦截增加被代理类的方法,和JDK动态代理中的invoke方法类似。通过Enhancer类的create()创建代理类。
    不同于JDK动态代理不需要添加额外的依赖,CGLIB实际上是一个开源项目,要使用的话需要手动添加依赖
<dependency>  <groupId>cglib</groupId>  <artifactId>cglib</artifactId>  <version>3.3.0</version></dependency>
自定义拦截器
public class DebugMethodInterceptor implements MethodInterceptor {    /**     * @param o           代理对象(增强的对象)     * @param method      被拦截的方法(需要增强的方法)     * @param args        方法入参     * @param methodProxy 用于调用原始方法     */    @Override    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {        System.out.println("<<<<<<<<<执行test方法之前");        Object result = methodProxy.invokeSuper(o, args);        System.out.println("<<<<<<<<<执行test方法之后");        return object;    }}
获取代理类
import net.sf.cglib.proxy.Enhancer;public class CglibProxyFactory {    public static Object getProxy(Class<?> clazz) {        // 创建动态代理增强类        Enhancer enhancer = new Enhancer();        // 设置类加载器        enhancer.setClassLoader(clazz.getClassLoader());        // 设置被代理类        enhancer.setSuperclass(clazz);        // 设置方法拦截器        enhancer.setCallback(new DebugMethodInterceptor());        // 创建代理类        return enhancer.create();    }}
实际使用
TestProxyService testProxyService =            (TestProxyService) CglibProxyFactory.getProxy(TestProxyServiceImpl.class);        testProxyService.test("测试代理");
这个控制台打印和上面一样。

JDK动态代理和CGLIB动态代理对比

  • JDK动态代理只能代理实现了接口的类或者直接代理接口,而CGLIB可以代理未实现任何接口的类。
    另外CGLIB动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因为不能代理声明为final类型的类和方法。就二者的效率来说,JDK动态代理更加优秀,随着JDK版本升级,这个优势更明显。
静态代理和动态代理的对比

    灵活性:动态代理更加灵活,不需要必须实现接口,可以直接代理实现类。并且可以不需要针对每个目标类创建一个代理类。另外静态代理中接口一旦增加新的方法目标对象和代理对象都要修改,很麻烦。JVM层面:静态代理编译时就将接口,实现类,代理类这些变成了一个个class文件。而动态代理是运行时动态生成类字节码,并且加载到JVM中。
IO模型详解


IO模型这块很难理解,需要计算机底层知识。我反正看了好几遍这篇文章,然后还查阅了很多资料。下面简单的鹦鹉学舌下。
何为I/O?


I/O即 输入/输出
我们先从计算机结构的角度解读一下I/O、
根据冯.诺依曼结构,计算机结构分为五大部分:运算器,控制器,存储器,输入设备,输出设备。


计算机结构

输入(比如键盘)和输出(比如显示器)设备都属于外部设备。网卡,硬盘这种可以属于输入设备,也可以属于输出设备。
输入设备向计算机输入数据,输出设备接收计算机输出的数据。
从计算机结构的视角来说,I/O描述了计算机系统与外部设备之间通信的过程。
我们再从应用程序的角度来解读一下I/O。
说一个操作系统相关的知识:为了保证操作系统的稳定性和安全性,一个进程的地址空间划分为用户空间内核空间
像是我们平常运行的应用程序都是运行在用户空间的,只有内核空间才能进行系统态级别的资源有关的操作,比如文件管理,进程通信,内存管理等等。也就是说,我们想要进行IO操作,一定是要以来内核空间的能力。
并且用户空间的程序不能直接访问内核空间。
当想要执行IO操作时,由于没有执行这些操作的权限,只能发起系统调用请求操作系统帮忙完成。
因此,用户进程想要执行IO操作的话,必须通过系统调用来简介访问内核空间。
我们在平常开发过程中接触最多的就是磁盘IO(读写文件)和网络IO(网络请求和响应)。
从应用程序的视角来看的话,我们的应用程序对操作系统的内核发起IO调用(系统调用),操作系统负责的内核执行具体的IO操作。也就是说,我们的应用程序实际上只是发起了IO操作的调用而已。具体IO的执行是由操作系统的内核来完成的。
当应用程序发起I/O调用后,会经历两个步骤:
    内核等待I/O设备准备好数据内核将数据从内核空间拷贝到用户空间
有哪些常见的IO模型?


UNIX系统下,IO模型一共有五种:
    同步阻塞I/O同步非阻塞I/OI/O多路复用信号驱动I/O异步I/O
Java中常见的三种IO模型


BIO(Blocking I/O)
BIO属于同步阻塞IO模型
同步阻塞IO模型中,应用程序发起read调用后,会一直阻塞,直到内核把数据拷贝到用户空间。


同步阻塞过程

在客户端连接数量不高的情况下, 是没问题的。但是当面对十万甚至百万级连接的时候,传统的BIO模型是无能为力的,因为我们需要一种更高效的I/O处理模型来应对更高的并发量。
NIO(Non-blocking或者New I/O)
java中的NIO是1.4引入的,对应Java.nio包,提供了Channel,Selector,Buffer等抽象。
NIO中的N可以理解为Non-blocking,不仅仅是new。它支持面向缓冲的,基于甬道I/O操作方法,对于高负载,高并发的网络应用应该使用NIO。
java中的NIO可以看做是I/O的多路复用模型,也有很多人认为Java中的NIO属于同步非阻塞IO模型。

同步非阻塞

同步非阻塞IO模型中,应用程序会一直发起read调用,等待数据从内核空间拷贝到用户空间的这段时间,线程依然是阻塞的,知道内核把数据拷贝到用户空间。
相比于同步阻塞IO模型,同步非阻塞IO模型确实有了很大改进,通过轮询操作避免了一直阻塞。
但是这种IO模型同样存在问题:应用程序不断进行I/O系统调用轮询数据是否已经准备好的过程是十分消耗CPU资源的。
这个时候,I/O多路复用模型就上场了。

I/O多路复用

IO多路复用模型中,线程首先发起select调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起read调用。read调用的过程(数据从内核空间->用户空间)还是阻塞的。
目前支持IO多路复用的系统调用,有select,epoll等等。select系统调用,目前几乎在所有的操作系统上都有支持。
    select调用:内核提供的系统调用,它支持一次性查询多个系统调用的可用状态。几乎所有的操作系统都支持。epoll调用:linux2.6内核,属于select调用的增强版,优化了IO的执行效率。

IO多路复用模型,通过减少无效的系统调用,减少了对cpu资源的消耗。
Java中的NIO,有一个非常重要的选择器的概念,也可以被称为多路复用器。通过它,只需要一个线程便可以管理多个客户端连接。当了客户端数据到了以后才会为其服务。


Java中NIO模型

AIO(Asynchronous I/O)
AIO也就是NIO2,java7中引入了NIO的改进版本NIO2,它是异步IO模型。异步IO是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成后操作系统会通知相应的线程进行后续的操作。

异步IO模型

目前来说AIO的应用还不是很广泛,Netty之前也尝试过使用AIO,不过又放弃了,这是因为Netty使用了AIO之后,在Linux系统上的性能并没有多少提升。
下面一张图总结BIO,NIO,AIO:

BIO,NIO,AIO

BigDecimal详解


阿里巴巴开发手册上提到:为了避免精度丢失,可以使用BigDecimal来进行浮点数的运算。这里就解释下浮点运算出现精度丢失的原因和BigDecimal的常见用法。
BigDecimal介绍


BigDecimal 可以实现浮点数的运算,不会造成精度丢失,通常情况下大部分需要浮点数精确运算结果的业务场景都是通过BigDecimal 来做的。
首先double和float会有精度问题,下面是demo:


double精度问题

为什么浮点数double,float运算的时候会有精度丢失风险?
这个和计算机保存浮点数的机制有很大关系,让我们知道计算机是二进制的,而且计算机在标识一个数字时,宽度是有限的,无限循环的小数存储在计算机时只能被截断。所以就会导致小数精度发生损失的情况。这也就解释了为什么浮点数没有办法用二进制精确表示。

BigDecimal的用处


阿里巴巴java开发手册上提到:浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用equals来比较。
具体原因就是我们上面的例子:
float a = 1.0f - 0.9f;float b = 0.9f - 0.8f;System.out.println(a);// 0.100000024System.out.println(b);// 0.099999964System.out.println(a == b);// false
如上,a==b就是false,同理a,b被包装类包装后,equals也是false。
BigDecimal的常见方法


加减乘除
    add方法用于两个BigDecimal对象相加。subtract方法用于两个BigDecimal对象相减。multiply方法用于两个BigDecimal对象相乘。divide方法用于两个BigDecimal对象相除。

这里有一个需要注意的点:divide方法有两种:一种是a.divide(b)。但是这种写法如果除不尽的话会抛出一个异常ArithmeticException(无法除尽出现无限循环小数的时候),所以我们最好使用另一种使用方式:
a.divide(b, 2, RoundingMode.HALF_UP)。这里的2是保留两位小数。后面是保留规则。
比较大小
a.compareTo();a,b都是BigDecimal对象。返回-1说明a<b,0说明a=b,1说明a>b.
保留几位小数
BigDecimal对象可以通过setScale方法设置保留几位小数和保留规则。
BigDecimal的使用注意事项


注意我们在使用BigDecimal的时候为了防止精度丢失,推荐使用她的BigDecimal(String value)构造方法或者 BigDecimal.valueOf(double val)静态方法来创建对象。
不推荐直接用BigDecimal(double d)的方式创建BigDecimal对象。
BigDecimal工具类分享


网上有一个使用人数比较多的BigDecimal工具类,提供了多个静态方法来简化BigDecimal的操作。 下面是工具类代码:
import java.math.BigDecimal;import java.math.RoundingMode;/** * 简化BigDecimal计算的小工具类 */public class BigDecimalUtil {    /**     * 默认除法运算精度     */    private static final int DEF_DIV_SCALE = 10;    private BigDecimalUtil() {    }    /**     * 提供精确的加法运算。     *     * @param v1 被加数     * @param v2 加数     * @return 两个参数的和     */    public static double add(double v1, double v2) {        BigDecimal b1 = BigDecimal.valueOf(v1);        BigDecimal b2 = BigDecimal.valueOf(v2);        return b1.add(b2).doubleValue();    }    /**     * 提供精确的减法运算。     *     * @param v1 被减数     * @param v2 减数     * @return 两个参数的差     */    public static double subtract(double v1, double v2) {        BigDecimal b1 = BigDecimal.valueOf(v1);        BigDecimal b2 = BigDecimal.valueOf(v2);        return b1.subtract(b2).doubleValue();    }    /**     * 提供精确的乘法运算。     *     * @param v1 被乘数     * @param v2 乘数     * @return 两个参数的积     */    public static double multiply(double v1, double v2) {        BigDecimal b1 = BigDecimal.valueOf(v1);        BigDecimal b2 = BigDecimal.valueOf(v2);        return b1.multiply(b2).doubleValue();    }    /**     * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到     * 小数点以后10位,以后的数字四舍五入。     *     * @param v1 被除数     * @param v2 除数     * @return 两个参数的商     */    public static double divide(double v1, double v2) {        return divide(v1, v2, DEF_DIV_SCALE);    }    /**     * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指     * 定精度,以后的数字四舍五入。     *     * @param v1    被除数     * @param v2    除数     * @param scale 表示表示需要精确到小数点以后几位。     * @return 两个参数的商     */    public static double divide(double v1, double v2, int scale) {        if (scale < 0) {            throw new IllegalArgumentException(                    "The scale must be a positive integer or zero");        }        BigDecimal b1 = BigDecimal.valueOf(v1);        BigDecimal b2 = BigDecimal.valueOf(v2);        return b1.divide(b2, scale, RoundingMode.HALF_UP).doubleValue();    }    /**     * 提供精确的小数位四舍五入处理。     *     * @param v     需要四舍五入的数字     * @param scale 小数点后保留几位     * @return 四舍五入后的结果     */    public static double round(double v, int scale) {        if (scale < 0) {            throw new IllegalArgumentException(                    "The scale must be a positive integer or zero");        }        BigDecimal b = BigDecimal.valueOf(v);        BigDecimal one = new BigDecimal("1");        return b.divide(one, scale, RoundingMode.HALF_UP).doubleValue();    }    /**     * 提供精确的类型转换(Float)     *     * @param v 需要被转换的数字     * @return 返回转换结果     */    public static float convertToFloat(double v) {        BigDecimal b = new BigDecimal(v);        return b.floatValue();    }    /**     * 提供精确的类型转换(Int)不进行四舍五入     *     * @param v 需要被转换的数字     * @return 返回转换结果     */    public static int convertsToInt(double v) {        BigDecimal b = new BigDecimal(v);        return b.intValue();    }    /**     * 提供精确的类型转换(Long)     *     * @param v 需要被转换的数字     * @return 返回转换结果     */    public static long convertsToLong(double v) {        BigDecimal b = new BigDecimal(v);        return b.longValue();    }    /**     * 返回两个数中大的一个值     *     * @param v1 需要被对比的第一个数     * @param v2 需要被对比的第二个数     * @return 返回两个数中大的一个值     */    public static double returnMax(double v1, double v2) {        BigDecimal b1 = new BigDecimal(v1);        BigDecimal b2 = new BigDecimal(v2);        return b1.max(b2).doubleValue();    }    /**     * 返回两个数中小的一个值     *     * @param v1 需要被对比的第一个数     * @param v2 需要被对比的第二个数     * @return 返回两个数中小的一个值     */    public static double returnMin(double v1, double v2) {        BigDecimal b1 = new BigDecimal(v1);        BigDecimal b2 = new BigDecimal(v2);        return b1.min(b2).doubleValue();    }    /**     * 精确对比两个数字     *     * @param v1 需要被对比的第一个数     * @param v2 需要被对比的第二个数     * @return 如果两个数一样则返回0,如果第一个数比第二个数大则返回1,反之返回-1     */    public static int compareTo(double v1, double v2) {        BigDecimal b1 = BigDecimal.valueOf(v1);        BigDecimal b2 = BigDecimal.valueOf(v2);        return b1.compareTo(b2);    }}总结


浮点数没有办法用二进制精确表示,因为存在精度丢失的风险。
不过java提供了BigDecimal来操作浮点数。BigDecimal的实现利用了BigInteger(用来操作大整数),所不同的是BigDecimal加入了小数位的概念。

本篇笔记就记到这里,如果对你有所帮助,记得点个喜欢点个关注哟~也祝大家工作顺利!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-26 02:51 , Processed in 0.069118 second(s), 23 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表