接口和抽象类的区别,如何选择?

典型回答 接口(Interface)和抽象类(Abstract Class)是面向对象编程中两个非常重要的概念,它们都可以用来实现抽象层。 接口: 1 2 3 public interface PayService { public void pay(PayRequest payRequest); } 抽象类: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public abstract class AbstractPayService implements PayService { @Override public void pay(PayRequest payRequest) { //前置检查 validateRequest(payRequest); //支付核心逻辑 doPay(payRequest); //后置处理 postPay(payRequest); } public abstract void doPay(PayRequest payRequest); private void postPay(PayRequest payRequest) { //支付成功的后置处理 } public void validateRequest(PayRequest payRequest) { //参数检查 } } 接口和抽象类的区别其实挺多的。比如以下这些: ...

March 22, 2026 · 1 min · santu

String、StringBuilder和StringBuffer的区别?

典型回答 String是不可变的,StringBuilder和StringBuffer是可变的。而StringBuffer是线程安全的,而StringBuilder是非线程安全的。 扩展知识 String的不可变性 ✅String是如何实现不可变的? 为什么设计成不可变的 ✅String为什么设计成不可变的? String的"+“是如何实现的 使用+拼接字符串,其实只是Java提供的一个语法糖, 那么,我们就来解一解这个语法糖,看看他的内部原理到底是如何实现的。 还是这样一段代码。我们把他生成的字节码进行反编译,看看结果。 1 2 3 String wechat = "Hollis"; String introduce = "Chuang"; String hollis = wechat + "," + introduce; 反编译后的内容如下,反编译工具为jad。 1 2 3 String wechat = "Hollis"; String introduce = "Chuang"; String hollis = (new StringBuilder()).append(wechat).append(",").append(introduce).toString(); 通过查看反编译以后的代码,我们可以发现,原来字符串常量在拼接过程中,是将String转成了StringBuilder后,使用其append方法进行处理的。 ...

March 22, 2026 · 2 min · santu

为什么对Java中的负数取绝对值结果不一定是正数?

典型回答 假如,我们要用Math.abs对一个Integer取绝对值的时候,如果用如下方式: 1 Math.abs(orderId.hashCode()); 得到的结果可能是个负数。原因要从Integer的取值范围说起,int的取值范围是-2^31 —— (2^31) - 1,即-2147483648 至 2147483647 那么,当我们使用abs取绝对值时候,想要取得-2147483648的绝对值,那应该是2147483648。但是,2147483648大于了2147483647,即超过了int的取值范围。这时候就会发生越界。 2147483647用二进制的补码表示是:01111111 11111111 11111111 11111111 这个数 +1 得到:10000000 00000000 00000000 00000000 这个二进制就是-2147483648的补码。 虽然,这种情况发生的概率很低,只有当要取绝对值的数字是-2147483648的时候,得到的数字还是个负数。 那么,如何解决这个问题呢? 既然是因为越界了导致最终结果变成负数,那就解决越界的问题就行了,那就是在取绝对值之前,把这个int类型转成long类型,这样就不会出现越界了。 如,前面我们取值逻辑修改为 1 Math.abs((long)orderId.hashCode()); 就万无一失了。 大家可以执行下以下代码: 1 2 3 public static void main(String[] args) { System.out.println(Math.abs((long)Integer.MIN_VALUE)); } 得到的结果就是: 2147483648 扩展知识 整型的取值范围 Java中的整型主要包含byte、short、int和long这四种,表示的数字范围也是从小到大的,之所以表示范围不同主要和他们存储数据时所占的字节数有关。 先来个简单的科普,1字节=8位(bit)。java中的整型属于有符号数。 先来看计算中8bit可以表示的数字: 最小值:10000000 (-128)(-2^7) 最大值:01111111(127)(2^7-1) 整型的这几个类型中, byte:byte用1个字节来存储,范围为-128(-2^7)到127(2^7-1),在变量初始化的时候,byte类型的默认值为0。 short:short用2个字节存储,范围为-32,768 (-2^15)到32,767 (2^15-1),在变量初始化的时候,short类型的默认值为0,一般情况下,因为Java本身转型的原因,可以直接写为0。 int:int用4个字节存储,范围为-2,147,483,648 (-2^31)到2,147,483,647 (2^31-1),在变量初始化的时候,int类型的默认值为0。 long:long用8个字节存储,范围为-9,223,372,036,854,775,808 (-2^63)到9,223,372,036, 854,775,807 (2^63-1),在变量初始化的时候,long类型的默认值为0L或0l,也可直接写为0。 超出范围怎么办 上面说过了,整型中,每个类型都有一定的表示范围,但是,在程序中有些计算会导致超出表示范围,即溢出。如以下代码: ...

March 22, 2026 · 1 min · santu

String a = _ab_; String b = _a_ + _b_; a == b 吗?

典型回答 在Java中,对于字符串使用==比较的是字符串对象的引用地址是否相同。 因为"ab"和"a"、“b"都是由字面量(““包裹的内容)组成的字符串,在编译之后,会把用”+“拼接的字面量直接合在一起。因此他们的最终都是"ab”,而字面值最终在字符串池只有一份,所以a == b的结果为true,因为它们指向的是同一个字符串对象。 ✅String、StringBuilder和StringBuffer的区别? 扩展知识 字面量 在计算机科学中,字面量(literal)是用于表达源代码中一个固定值的表示法(notation)。几乎所有计算机编程语言都具有对基本值的字面量表示,诸如:整数、浮点数以及字符串;而有很多也对布尔类型和字符类型的值也支持字面量表示;还有一些甚至对枚举类型的元素以及像数组、记录和对象等复合类型的值也支持字面量表示法。 以上是关于计算机科学中关于字面量的解释,并不是很容易理解。说简单点,字面量就是指由字母、数字等构成的字符串或者数值。 字面量只可以右值出现,所谓右值是指等号右边的值,如:int a=123这里的a为左值,123为右值。在这个例子中123就是字面量。 1 2 int a = 123; String s = "hollis"; 上面的代码事例中,123和hollis都是字面量。 JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化。为了减少在JVM中创建的字符串的数量,字符串类维护了一个字符串常量池。 在JVM中,有一块区域是运行时常量池,主要用来存储编译期生成的各种字面量和符号引用。 ✅运行时常量池和字符串常量池的关系是什么? 了解Class文件结构或者做过Java代码的反编译的朋友可能都知道,在java代码被javac编译之后,文件结构中是包含一部分Constant pool的。比如以下代码: 1 2 3 public static void main(String[] args) { String s = "Hollis"; } 经过编译后,常量池内容如下: 1 2 3 4 5 6 7 8 9 10 11 Constant pool: #1 = Methodref #4.#20 // java/lang/Object."<init>":()V #2 = String #21 // Hollis #3 = Class #22 // StringDemo #4 = Class #23 // java/lang/Object ... #16 = Utf8 s .. #21 = Utf8 Hollis #22 = Utf8 StringDemo #23 = Utf8 java/lang/Object 上面的Class文件中的常量池中,比较重要的几个内容: ...

March 22, 2026 · 1 min · santu

RPC接口返回中,使用基本类型还是包装类?

典型回答 使用包装类,不要使用基本类型,比如某个字段表示费率的Float rate,在接口中返回时,如果出现接口异常的情况,那么可能会返回默认值,float的话返回的是0.0,而Float返回的是null。 在接口中,为了避免发生歧义,建议使用对象,因为他默认值是null,当看到null的时候,我们明确的知道他是出错了,但是看到0.0的时候,你不知道是因为出错返回的0.0,还是就是不出错真的返回了0.0,虽然可以用其他的字段如错误码或者getSuccess判断,但是还是尽量减少歧义的可能 知识扩展 在接口定义的时候,如何定义一个字段表示是否成功? 以下四种: 1 2 3 4 boolean success Boolean success boolean isSuccess Boolean isSuccess 建议使用第2种: 1 Boolean success 首先,作为接口的返回对象的参数,这个字段不应该有不确定的值,而Boolean类型的默认值是null,而boolean的默认值是false,所以,当拿到一个false的时候,你就不知道是真的false了,还是因为出了问题而默认返回的false。 其他,关于参数名称,要使用success还是isSuccess,这一点在阿里巴巴Java开发手册中有明确规定和解释: 【强制】 POJO 类中的任何布尔类型的变量,都不要加 is,否则部分框架解析会引起序列化错误。 反例: 定义为基本数据类型 boolean isSuccess;的属性,它的方法也是 isSuccess(), RPC 框架在反向解析的时候, “ 以为” 对应的属性名称是 success,导致属性获取不到,进而抛出 异常。

March 22, 2026 · 1 min · santu

String有长度限制吗?是多少?

典型回答 有,编译期和运行期不一样。 编译期需要用CONSTANT_Utf8_info 结构用于表示字符串常量的值,而这个结构是有长度限制,他的限制是65535。 运行期,String的length参数是Int类型的,那么也就是说,String定义的时候,最大支持的长度就是int的最大范围值。根据Integer类的定义,java.lang.Integer#MAX_VALUE的最大值是2^31 - 1; 扩展知识 常量池限制 我们知道,javac是将Java文件编译成class文件的一个命令,那么在Class文件生成过程中,就需要遵守一定的格式。 根据《Java虚拟机规范》中第4.4章节常量池的定义,CONSTANT_String_info 用于表示 java.lang.String 类型的常量对象,格式如下: 1 2 3 4 CONSTANT_String_info { u1 tag; u2 string_index; } 其中,string_index 项的值必须是对常量池的有效索引, 常量池在该索引处的项必须是 CONSTANT_Utf8_info 结构,表示一组 Unicode 码点序列,这组 Unicode 码点序列最终会被初始化为一个 String 对象。 CONSTANT_Utf8_info 结构用于表示字符串常量的值: 1 2 3 4 5 CONSTANT_Utf8_info { u1 tag; u2 length; u1 bytes[length]; } 其中,length则指明了 bytes[]数组的长度,其类型为u2, 通过翻阅《规范》,我们可以获悉。u2表示两个字节的无符号数,那么1个字节有8位,2个字节就有16位。 16位无符号数可表示的最大值位2^16 - 1 = 65535。 也就是说,Class文件中常量池的格式规定了,其字符串常量的长度不能超过65535。 那么,我们尝试使用以下方式定义字符串: 1 String s = "11111...1111";//其中有65535个字符"1" 尝试使用javac编译,同样会得到"错误: 常量字符串过长",那么原因是什么呢? ...

March 22, 2026 · 2 min · santu

常见的字符编码有哪些?有什么区别?

典型回答 就像电报只能发出”滴”和”答”声一样,计算机只认识0和1两种字符,但是,人类的文字是多种多样的,如何把人类的文字转换成计算机认识的01字符呢,这个过程同样需要通过字符编码。 字符编码(Character encoding)是一套法则,使用该法则能够对自然语言的字符的一个集合(如字母表或音节表),与其他东西的一个集合(如号码或电脉冲)进行配对。 和摩尔斯电码功能类似,上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定,这被称为 ASCII 码,一直沿用至今。 由于ASCII只有128个字符,虽然对于英文字符都可以表示了,但是世界上还有很多其他的文字他是没办法表示的,所以需要一种更加全面的字符编码。 于是又出现了Unicode字符集(常见的Unicode Transformation Format有:UTF-7, UTF-7.5, UTF-8,UTF-16, 以及 UTF-32),除此之外还有一些常用的中文编码有GBK,GB2312,GB18030等。 扩展知识 Unicode和UTF-8有啥关系? Unicode(中文:万国码、国际码、统一码、单一码)是计算机科学领域里的一项业界标准。它对世界上大部分的文字系统进行了整理、编码,使得计算机可以用更为简单的方式来呈现和处理文字。 Unicode备受认可,并广泛地应用于计算机软件的国际化与本地化过程。有很多新科技,如可扩展置标语言(Extensible Markup Language,简称:XML)、Java编程语言以及现代的操作系统,都采用Unicode编码。 Unicode是一套通用的字符集,包含世界上的大部分文字,也就是说,Unicode是可以表示中文的。 但是,Unicode虽然统一了全世界字符的编码,但没有规定如何存储。 因为如果Unicode统一规定,每个符号就要用三个或四个字节表示,因为字符太多,只能用这么多字节才能表示完全。一旦这么规定,那么每个英文字母前都必然有二到三个字节是0,因为所有英文字母在ASCII中都有,都可以用一个字节表示,剩余字节位置就要补充0。如果这样,文本文件的大小会因此大出二三倍,这对于存储来说是极大的浪费。 为了解决这个问题,就出现了一些中间格式的字符集,他们被称为通用转换格式,**即UTF(Unicode Transformation Format)。**常见的UTF格式有:UTF-7, UTF-7.5, UTF-8,UTF-16, 以及 UTF-32。 UTF-8 使用一至四个字节为每个字符编码 UTF-16 使用二或四个字节为每个字符编码 UTF-32 使用四个字节为每个字符编码 所以我们可以说,UTF-8、UTF-16等都是 Unicode 的一种实现方式。 有了UTF-8,为什么要出现GBK 因为UTF-8是Unicode的一种实现,所以他包含了世界上的所有文字的编码,他采用的是1-4字节进行编码。 对于那些排在前面优先纳入的文字,可能就优先使用1字节、2字节存储了,对于后纳入的文字,就要使用3字节或者4字节存储了。 正是因为UTF-8太全了,所以那些晚一些纳入的字符,在UTF-8中的存储所占的字节数可能就会多一些,那他的存储空间要求就会很大。 对于常用的汉字,在UTF-8中采用3字节进行编码,但是如果有一种只包含中文和ASCII的编码的话,就不需要使用3个字节,可能2个字节就够了。 对于大部分网站来说,基本都是只服务一个国家或者地区的,比如一个中国的网站,一般会出现简体字和繁体字以及一些英文字符,很少会出现日语或者韩文的。 也是出于这样的考虑,中国国家标准总局于1981年制定并实施了 GB 2312-80 编码,即中华人民共和国国家标准简体中文字符集。后来厂商微软利用GB 2312-80未使用的编码空间,收录GB 13000.1-93全部字符制定了GBK编码。 有了标准中文字符集,如果是一个纯中文网站,就可以可以采用这种编码方式,这样可以大大节省一些存储空间的。 常用的中文编码有GBK,GB2312,GB18030等,最常用的是GBK。 GB2312(1980年):16位字符集,收录有6763个简体汉字,682个符号,共7445个字符; 优点:适用于简体中文环境,属于中国国家标准,通行于大陆,新加坡等地也使用此编码; 缺点:不兼容繁体中文,其汉字集合过少。 GBK(1995年):16位字符集,收录有21003个汉字,883个符号,共21886个字符; 优点:适用于简繁中文共存的环境,为简体Windows所使用,向下完全兼容gb2312,向上支持 ISO-10646 国际标准 ;所有字符都可以一对一映射到unicode2.0上; 缺点:不属于官方标准,和big5之间需要转换;很多搜索引擎都不能很好地支持GBK汉字。 GB18030(2000年):32位字符集;收录了27484个汉字,同时收录了藏文、蒙文、维吾尔文等主要的少数民族文字。 优点:可以收录所有你能想到的文字和符号,属于中国最新的国家标准; 缺点:目前支持它的软件较少。 为什么会出现乱码 文件里面的内容归根到底都是有0101组成的,至于0101的二进制码如何转成人们可以理解的字符串,则是需要通过规定好的字符编码标准进行转换才可以。 ...

March 22, 2026 · 1 min · santu

Lambda表达式是如何实现的?

关于lambda表达式,有人可能会有质疑,因为网上有人说他并不是语法糖。其实我想纠正下这个说法。Labmda表达式不是匿名内部类的语法糖,但是他也是一个语法糖。实现方式其实是依赖了几个JVM底层提供的lambda相关api。 先来看一个简单的lambda表达式。遍历一个list: 1 2 3 4 5 public static void main(String... args) { List<String> strList = ImmutableList.of("Hollis", "公众号:Hollis", "博客:www.hollischuang.com"); strList.forEach( s -> { System.out.println(s); } ); } 为啥说他并不是内部类的语法糖呢,前面讲内部类我们说过,内部类在编译之后会有两个class文件,但是,包含lambda表达式的类编译后只有一个文件。 ✅说几个常见的语法糖? 反编译后代码如下: 1 2 3 4 5 6 7 8 public static /* varargs */ void main(String ... args) { ImmutableList strList = ImmutableList.of((Object)"Hollis", (Object)"\u516c\u4f17\u53f7\uff1aHollis", (Object)"\u535a\u5ba2\uff1awww.hollischuang.com"); strList.forEach((Consumer<String>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)V, lambda$main$0(java.lang.String ), (Ljava/lang/String;)V)()); } private static /* synthetic */ void lambda$main$0(String s) { System.out.println(s); } 可以看到,在forEach方法中,其实是调用了java.lang.invoke.LambdaMetafactory#metafactory方法,该方法的第5个参数implMethod指定了方法实现。可以看到这里其实是调用了一个lambda$main$0方法进行了输出。 ...

March 22, 2026 · 1 min · santu

说几个常见的语法糖?

典型回答 语法糖(Syntactic sugar),指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。 虽然Java中有很多语法糖,但是Java虚拟机并不支持这些语法糖,所以这些语法糖在编译阶段就会被还原成简单的基础语法结构,这样才能被虚拟机识别,这个过程就是解语法糖。 如果看过Java虚拟机的源码,就会发现在编译过程中有一个重要的步骤就是调用desugar(),这个方法就是负责解语法糖的实现。 常见的语法糖有 switch支持枚举及字符串、泛型、条件编译、断言、可变参数、自动装箱/拆箱、枚举、内部类、增强for循环、try-with-resources语句、lambda表达式等。 知识扩展 如何解语法糖? 语法糖的存在主要是方便开发人员使用。但其实,Java虚拟机并不支持这些语法糖。这些语法糖在编译阶段就会被还原成简单的基础语法结构,这个过程就是解语法糖。 说到编译,大家肯定都知道,Java语言中javac命令可以将后缀名为.java的源文件编译为后缀名为.class的可以运行于Java虚拟机的字节码。如果你去看com.sun.tools.javac.main.JavaCompiler的源码,你会发现在compile()中有一个步骤就是调用desugar(),这个方法就是负责解语法糖的实现的。 糖块一、 switch 支持 String 与枚举 前面提到过,从Java 7 开始,Java语言中的语法糖在逐渐丰富,其中一个比较重要的就是Java 7中switch开始支持String。 在开始coding之前先科普下,Java中的switch自身原本就支持基本类型。比如int、char等。对于int类型,直接进行数值的比较。对于char类型则是比较其ascii码。所以,对于编译器来说,switch中其实只能使用整型,任何类型的比较都要转换成整型。比如byte。short,char(asckii码是整型)以及int。 那么接下来看下switch对String得支持,有以下代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class SwitchDemoString { public static void main(String[] args) { String str = "world"; switch (str) { case "hello": System.out.println("hello"); break; case "world": System.out.println("world"); break; default: break; } } } 反编译后内容如下: ...

March 22, 2026 · 8 min · santu

什么是类型擦除?

典型回答 类型擦除是Java在处理泛型的一种方式,如Java的编译器在编译以下代码时: 1 2 3 4 5 6 7 8 public class Foo<T> { T bar; void doSth(T param) { } }; Foo<String> f1; Foo<Integer> f2; 在编译后的字节码文件中,会把泛型的信息擦除掉: 1 2 3 4 5 public class Foo { Object bar; void doSth(Object param) { } }; 也就是说,在代码中的Foo 和 Foo使用的类,经过编译后都是同一个类。 所以说泛型技术实际上是Java语言的一颗语法糖,因为泛型经过编译器处理之后就被擦除了。 这种擦除的过程,被称之为——类型擦除。所以类型擦除指的是通过类型参数合并,将泛型类型实例关联到同一份字节码上。编译器只为泛型类型生成一份字节码,并将其实例关联到这份字节码上。类型擦除的关键在于从泛型类型中清除类型参数的相关信息,并且在必要的时候添加类型检查和类型转换的方法。 类型擦除可以简单的理解为将泛型java代码转换为普通java代码,只不过编译器更直接点,将泛型java代码直接转换成普通java字节码。 扩展知识 C语言对泛型的支持 泛型是一种编程范式,在不同的语言和编译器中的实现和支持方式都不一样。 通常情况下,一个编译器处理泛型有多种方式,在C++中,当编译器对以下代码编译时: 1 2 3 4 5 6 7 8 9 10 template<typename T> struct Foo { T bar; void doSth(T param) { } }; Foo<int> f1; Foo<float> f2; 当编译器对其进行编译时,编译器发现要用到Foo和Foo,这时候就会为每一个泛型类新生成一份执行代码。相当于新创建了如下两个类: ...

March 22, 2026 · 1 min · santu

留言给博主