优秀的后端应该知道的易错点

举报
JavaSouth南哥 发表于 2024/12/12 18:16:14 2024/12/12
【摘要】 TIOBE编程社区给出了 2024 年编程语言流行度的指标,南哥看到我们的 Java 现在是排第三~

先赞后看,Java进阶一大半

TIOBE 编程社区给出了 2024 年编程语言流行度的指标,南哥看到我们的 Java 现在是排第三~

在这里插入图片描述

各位hao,我是南哥,相信对你通关面试、拿下Offer有所帮助。

⭐⭐⭐一份南哥编写的《Java学习/进阶/面试指南》:https://github/JavaSouth

1. 数据类型

1.1 static修饰的变量

大家在玩Java时有没发现,下面这样一个对象,我们即使没有给变量赋值,在创建它后这个变量依旧会有默认值

class A {
    int a;
}

System.out.println(new A().a);
程序执行结果:
0

有时前端同学要求后端给个默认值0,我们甚至不用动手,Java编译器就把活给干完。

这实际上是Java语言的一个特性,对于实例变量即成员变量,如果是基本数据类型都会有一个默认值。不同的基本类型默认值不同,我们看看以下各种基本类型的默认值。

int a; //0
short b; //0
long c; //0
float d; //0.0
double e; //0.0
boolean f; //false
byte g; //0
char h; //空字符

1.2 自动类型提升

(1)Java中的byte、short、char进行数学计算时都会提升为int类型,很容易忽略的基础知识,南哥慢慢道来。

以下代码的运行正常吗?

byte b1 = 1, b2 = 2, b3;
b3 = b1 + b2;

答案在你意料之中,就是编译报错。

# 报错内容
java: 不兼容的类型: 从int转换到byte可能会有损失

既然byte、short、char进行数学计算时都会提升为int类型,那我们就需要在运行过程中把结果转换成byte类型。正确的做法如下。

b3 = (byte)(b1 + b2);

(2)但假如byte变量是这样的写法,我们给b1和b2都加个final,很神奇,编译不会报错。

final byte b1 = 1, b2 = 2, b3;
b3 = b1 + b2;

这种情况是一个特殊情况,Java编译器会为其进行特殊处理,我们称它为编译时常量表达式的求值。b1、b2、b3都是常量值,b3在编译阶段就会被编译器进行赋值,不会涉及到上面我们提到的数学计算提升为int类型,也就不会编译错误。

(3)但如果是这种情况呢?

final byte b1 = 1; byte b2 = 2, b3;
b3 = b1 + b2;

以上两个byte变量,只有一个final修饰,也就是说对b3赋值运算不能在编译时进行,那这段代码依旧会报错,我们还是需要把结果转换为byte类型。

正确做法如下。

b3 = (byte)(b1 + b2);

1.3 byte溢出

byte类型的数据范围在-128 ~ 127,当这个值超过127会转变成 - 128。为什么呢?

byte i = 127;
System.out.println(++i);
程序执行结果:
-128

byte类型的最大值127在二进制中表示为01111111,当我们对127的值增加1时,每位加1后都会产生进位,导致的结果就是所有的位都会翻转(从01111111变成10000000),而10000000十进制的表示就是-128。

1.4 Bollean赋值

业务开发编写最多就是条件语句了,特别在迭代年代比较旧的老项目,一套接一套的if语句。

既然见识了那么多条件语句,那以下代码的执行结果是什么?

Boolean flag = false;
if (flag = true) {
    System.out.println("true");
}
else {
    System.out.println("false");
}

在Java里,条件判断是有赋值的功能,try语句同样也有。此时falg在条件判断里被赋值了。

程序执行结果:
true

2. 程序运算

2.1 三元运算符

三元运算符的坑,相信不少南友遇到过。。。我们来看看三元运算符是什么?

Java中的三元运算符是一种简洁的条件表达式工具,其语法格式为:条件 ? 表达式1 : 表达式2。

如果条件为真(true),则表达式的结果是表达式1;如果为假(false),则结果是表达式2

假如是这种情况呢,南哥问:o1最终的数据类型是什么?

Object o1 = true ? new Integer(1) : new Double(2.0);

上面的代码行其实等同于这一行。

Object o1 = true ? new Double(1.0) : new Double(2.0);

三元运算符的一个非常关键的细节就是类型的统一化。Double类型的数据范围更大于Interger类型,所以Java编译器会对值类型进行类型提升,最终把Integer类型提升为Double类型。

2.2 自增问题

下面是南哥编写的两个i++自增的易错问题,面试考核经常出现在笔试题。

(1)南哥第一问:以下代码执行的结果是什么?

int i = 0;
i = i++ + i; 
程序执行结果:
1

(2)南哥第二问:以下代码执行的结果是什么?

int i = 0;
i = i++;
System.out.println(i);
程序执行结果:
0

2.3 String对象

我们创建一个String对象,JVM在背后实际上做了很多功夫,String对象在常量池、堆内存都有可能存在。我们具体问题来具体分析下。

(1)以下代码段不包含引用类型,只是单纯的字面量拼接,所以只会创建一个对象存在于常量池中。

String s = "JavaProGuide" + "南哥" + 666;

(2)以下代码段包含了引用类型,一共创建了3个对象,猜对了吗?

String s = "Hello";
s = s + " world!"

“Hello”、" world!"都属于字面量,所以它们都会被加入到Java字符串常量池中。

s + " world!"这么一个代码段涉及了引用类型,所以它在内存里创建了一个新的String对象,并不存在于常量池,而是存在于堆内存里。

(3)以下代码段一共创建了两个对象,分别存在于常量池、堆内存。

首先new对象会把该String对象放到堆内存里,而过程中会先检查常量池是否存在JavaProGuide

String str = new String("JavaProGuide");

3. 抽象类

3.1 子类调用父类

现在有IDEA集成开发环境,可以给大家实时提醒哪个地方编译错误,但假如要大家用.txt文件编写程序呢。南哥问:现在这段代码错在了哪?

class Base {
    public Base(String s) {
        System.out.print("B");
    }
}

public class Derived extends Base {
    public Derived (String s) {
        System.out.print("D");
    }
    public static void main(String[] args) {
        new Derived("C");
    }
}

假如父类和子类同时拥有有参构造方法,子类的构造方法必须显性地调用父类的构造方法,否则会编译错误。所以正常的写法应该是这样。

    public Derived (String s) {
        super(s);  
        System.out.print("D");
    }

另外大家还需要注意一点,调用父类的构造方法必须在子类构造方法的第一行,调用父类的构造方法也只能出现在子类的构造方法上,否则也会是编译报错。

3.2 子类访问父类

如下代码,一共有两处编译错误。提示:错误在Child类里,能快速找出来吗?

class Parent {
    public static String staticVar = "Static Variable from Parent";
    private static String privateStaticVar = "Private Static Variable from Parent";

    public static void staticMethod() {
        System.out.println(staticVar);
    }

    private static void privateStaticMethod() {
        System.out.println(privateStaticVar);
    }
}

class Child extends Parent {
    public void staticMethod() {
        System.out.println("Static method in Child");
    }

    public void display() {
        System.out.println(staticVar);

        System.out.println(privateStaticVar);
        privateStaticMethod();
        
        staticMethod();
}

(1)父类的私有变量、私有方法,子类是有继承的,但是不能访问。所以Child.display()里的以下调用是编译错误的。

System.out.println(privateStaticVar);
privateStaticMethod();

(2)子类可以继承,同时也可以访问父类的static变量、方法。但父类的static方法大家需要注意,子类是不能直接覆盖的,所以以下代码会编译错误。

    public void staticMethod() {
        System.out.println("Static method in Child");
    }

正确的做法是为该方法添加一个static修饰符,代表这是子类的一个新方法。这种写法叫做方法隐藏,子类和父类中都有一个相同名称和参数的静态方法时,子类的方法将隐藏父类的方法。

    public static void staticMethod() {
        System.out.println("Static method in Child");
    }

另外如果父类的方法使用final修饰,子类也是不能覆盖的。

3.3 父类不可访问的方法

紧跟着上文代码的例子,父类的方法同样使用static修饰,子类的privateStaticMethod方法算不算覆盖父类的方法呢?有没有编译报错?

class Parent {
    public static String staticVar = "Static Variable from Parent";
    private static String privateStaticVar = "Private Static Variable from Parent";

    private static void privateStaticMethod() {
        System.out.println(privateStaticVar);
    }
}

class Child extends Parent {
    public void privateStaticMethod() {
        System.out.println(staticVar);
    }
}

答案是编译正常。

父类中不可访问的方法,子类编写相同名称和参数的方法并不算覆盖。父类的方法都不能访问了,也就没有覆盖这一说法了。。。

4. 接口

4.1 访问修饰符的区别

接口和抽象类有三个方面的区别,分布是类的修饰、方法的修饰、变量的修饰。我们往下看看。

(1)类

接口使用interface修饰,而抽象类使用abstract修饰。当它们作为外部类时,只能使用public、default修饰,不能使用private修饰。

(2)方法

普通接口方法只能由public abstractdefaultstatic修饰。

抽象接口方法可以由所有修饰符修饰,除了final。

总结下,它们两者也有共同点,就是都不能使用final修饰。

(3)变量

普通接口变量只能由public static final修饰。

抽象接口变量可以由所有修饰符修饰。

4.2 静态分派

这算是一个很偏的知识点了,如下代码有三个名为getType的重载方法,它们的返回类型相同、方法名也相同,只有入参类型不同。

南哥问:程序执行结果是什么?

public class Test {
    public static void main(String[] args) {
        for(Collection<?> collection: collections) {
            System.out.println(getType(collection));
        }
    }
    
    public static final Collection<?>[] collections = {new HashSet<String>(), new ArrayList<String>()};

    public static String getType(Collection<?> collection) {
        return "Super:collection";
    }
    public static String getType(List<?> list) {
        return "Super:list";
    }
    public String getType(ArrayList<?> list) {
        return "Super:arrayList";
    }
}

南哥给大家这么一行代码:Collection<?> collection = new ArrayList<Integer>()左边Collection<?>其实是静态类型,右边的new ArrayList<Integer>()其实是动态类型。

编译器在处理重载方法时,是根据参数的静态类型作为判断依据,而不是根据动态类型。collections数组里面的所有实例的静态类型都是Collection<?>getType方法也都是执行上文的第一个重载方法。

# 程序员执行结果
Super:collection
Super:collection

我是南哥,南就南在Get到你的点赞点赞点赞。

在这里插入图片描述

看了就赞,Java进阶一大半。点赞 | 收藏 | 关注,各位的支持就是我创作的最大动力❤️

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。