Java后端系统学习路线--Java基础(一)

128 阅读4分钟

Java后端系统学习路线--Java基础(一),码云仓库地址:gitee.com/qinstudy/ja…

1、Java基础之switch/case语法陷阱

小汪:榜哥,快一个多月没见了,最近在忙呢?

大榜:最近给自己安排了一个Java后端系统学习的路线图,这段时间在按照路线图,按部就班地推进和学习。这2周抽空回顾了下Java基础语法,还是古人总结的好:温故而知新。

小汪:我想起来了,是你上次总结的Java后端的系统学习路线图吗?

大榜:你的记性不错哈,就是这个,我目前正在学习Java基础部分。

小汪:温故而知新,榜哥有新感悟啊,给大伙分享下呗!

大榜:我目前虽然有2年Java开发的工作经验,但有时候感觉自己基础不扎实,于是就制定了这个后端的系统学习路线图,进行回炉学习、查漏补缺。所以,我主要关注的是自己容易踩坑的地方。

小汪:榜哥都容易踩坑的地方,那我更加容易踩坑,快分享给我们把。

大榜:Java中的switch、case的用法,就容易踩坑。这是反例:

public static void main(String[] args) {
        /**
         * 每条case语句后面,都应该跟break语句,否则会继续执行后面case中的代码。
         * 下面的语句输出为:
         *   "1"
         *   "2"
         *   "3"
         *   "默认"
         */
        int a = 1;
        switch (a) {
            case 1:
                System.out.println("1");
//                break;
            case 2:
                System.out.println("2");
//                break;
            case 3:
                System.out.println("3");
//                break;
            default:
                System.out.println("默认");
                break;
        }
​
    }

由上面的输出结果,我们可以得出:当每条case语句后面,没有break语句时,程序会继续执行后面case中的代码。

小汪:我刚刚去运行了下,如果不加上break语句或者return语句,确实会输出所有case下的结果。这确实是个坑点,我下次不会踩了。那还有什么踩坑点吗?

2、Java基础之封装

大榜:这次回炉学习,我对 面向对象的三大特性(封装、继承、多态)中的封装,进行了实践。这是没有封装的Point类。

package com.programming.logic.p3.p3_1_3;
​
/**
 * 使用public修饰符来修饰实例变量
 * @author qinxubang
 * @Date 2022/5/15 8:23
 */
class Point {
    /**
     * 使用public修饰符来修饰实例变量,程序员可以直接访问变量,没有办法进行参数检查和控制,程序员容易误操作。
     */
    public int x;
    public int y;
​
    /**
     * 求平面轴中一个点到坐标原点的距离
     * @return
     */
    public double distance() {
        return Math.sqrt((x * x + y * y));
    }
​
}
​
class PointTest {
    public static void main(String[] args) {
        Point point = new Point();
        point.x = 2;
        point.y = 3;
        System.out.println("求平面轴中一个点到坐标原点的距离为:");
        System.out.println(point.distance());
    }
}

可以看到,Point类中的实例变量,是使用public修饰符。使用public修饰符来修饰实例变量,程序员可以直接访问变量,没有办法进行参数检查和控制,程序员容易误操作。

接下来,我们使用private修饰实例变量,并提供setter、getter方法,并且setter方法中会对传入的参数进行校验,这样就避免了误操作。让我们来看看代码:

package com.programming.logic.p3.p3_1_3;
​
/**
 * 编写类的实现者,通过 private 封装和隐藏内部实现细节,而调用者只需要关心public就可以了。
 * 通过 private 封装和隐藏内部的实现细节,避免误操作,是计算机程序的一种基本思维方式。
 *
 * @author qinxubang
 * @Date 2022/5/15 8:31
 */public class Point3 {
​
    /**
     * 使用 private修饰符来修饰实例变量。通过set、get函数调用可以封装内部数据,避免误操作,所以我们一般不将成员变量定义为public
     */
    private int x;
    private int y;
​
    /* --------------- 构造函数的重载:无参构造函数、有参构造函数 --------------------*/
    /**
     * 定义无参构造函数
     */
    public Point3() {
        this(1, 1);
    }
​
    /**
     * 定义两个参数的构造函数
     * @param x
     * @param y
     */
    public Point3(int x, int y) {
        this.x = x;
        this.y = y;
    }
​
    /*------------------------------------*/
​
    /**
     * 这个例子中,我们将实例变量声明为private,通过set、get方法,可以在方法中对参数进行校验。
     * 而将实例变量声明为public,可以直接访问变量,但是没有办法进行参数检查和控制。
     * @param x
     */
    public void setX(int x) {
        if (x <= 0) {
            System.out.println("横坐标x 小于0,重新设置x的坐标为0");
            x = 0;
        }
        this.x = x;
    }
​
    public void setY(int y) {
        if (y <= 0) {
            System.out.println("纵坐标x 小于0,重新设置y的坐标为0");
            y = 0;
        }
        this.y = y;
    }
​
    /**
     * 求平面轴中一个点到坐标原点的距离
     * @return
     */
    public double distance() {
        return Math.sqrt((x * x + y * y));
    }
​
    /**
     * 求两点之间的距离
     * @param p
     * @return
     */
    public double distance(Point3 p) {
        return Math.sqrt(Math.pow(x - p.getX(), 2) + Math.pow(y - p.getY(), 2));
    }
​
}
​
​
class PointTest3 {
    public static void main(String[] args) {
//        Point3 point3 = new Point3(3, 4);
        Point3 point3 = new Point3();
​
        System.out.println("求平面轴中一个点到坐标原点的距离为:");
        System.out.println(point3.distance());
    }
}

上面的Point3类,是我们编写的类,通过 private 封装和隐藏 实例变量x、y的实现细节;而PointTest3作为调用者,只需要心public修饰的setter、getter就可以了。

3、Java基础之类的组合

小汪:以前看Java教科书,只知道书上是这么写的,没有做对比分析。榜哥这么一分析,我感觉理解又加深了。

大榜:你小子别夸了,大家一起学习进步!Java基础中还学习了类的组合,具体来说,就是一个类由其他类组合而成。举个栗子,刚才我们介绍了Point类,表示点的类,那我们是不是可以定义一个 线的类,姑且叫做Line,我们都知道一条线是由两个点组成,所以这个线的类中,应该有2个Point实例变量,这个没问题把。

小汪:没问题,两个点构成一条线,所以这个Line类,应该有2个实例变量,一个实例变量是startPoint,另一个实例变量是endPoint。

大榜:理解得挺快啊,这么快就跟上我的节奏了,那我们接着往下说。我们在Line类中定义一个方法:求线的长度,也就是求两点之间的距离,进一步转发为求两个Point对象之间的距离。在Line类中,我们来看下代码:

/**
 * 类的组合:Line类,由两个Point类组成,我们称之为类的组合
 *
 * Line由两个Point组成,length方法是计算线的长度,length方法是调用;Point计算距离的方法来获取线的长度。
 * 可以看出,在设计线时,我们考虑的层次是点,而不是点的内部细节。
 * 总结:每个类封装其内部细节,对外提供高层次的功能,使得其他类Line可以在更高层次上考虑和解决问题,是程序设计的一种基本思维方式。
 *
 * 分解现实问题中涉及的概念以及概念之间的关系,将概念表示为多个类,通过类之间的组合来表达更为复杂的概念以及概念之间的关系,是计算机程序的一种基本的思维方式。
 * 我们可以把二进制和运算看作一,将基本数据类型看作二,把基本数据类型形成的类看作三,类的组合和类的继承则是三生万物。
 *
 *
 * @author qinxubang
 * @Date 2022/5/15 16:35
 */
public class Line {
    private Point3 startPoint;
​
    private Point3 endPoint;
​
    public Line() {}
​
    public Line(Point3 startPoint, Point3 endPoint) {
        this.startPoint = startPoint;
        this.endPoint = endPoint;
    }
​
    public double length() {
        // 参数校验
        if (startPoint != null && endPoint != null) {
            return startPoint.distance(endPoint);
        } else {
            System.out.println("传入的Point参数为null");
            return -1;
        }
​
    }
​
    public static void main(String[] args) {
        Point3 startPoint = new Point3(2, 3);
        Point3 endPoint = new Point3(3, 4);
​
        Line line = new Line(startPoint, endPoint);
//        Line line = new Line();
        System.out.println("两点之间的距离为:");
        System.out.println(line.length());
    }
​
}

可以看到,Line由两个Point组成,length方法是计算线的长度。我们在设计线时,我们考虑的层次是点,而不是点的内部细节。进一步总结:每个类封装其内部细节,对外提供高层次的功能,使得其他类(如Line类)可以在更高层次上考虑和解决问题,是程序设计的一种基本思维方式。

小汪:这种设计线、点的思想,确实需要学习,增加自己对现实事物的抽象能力。

4、Java基础之继承、类加载和对象创建

大榜:在Java基础中,我重新回顾了类的继承。继承有几个注意点:

注意1:子类只继承了父类的非private的属性和方法,父类private的属性和方法,子类无法继承得到。父类有的属性和行为 子类都有,但是,子类可以增加子类特有的属性和行为;而且父类的某些行为,子类的实现方式可能与父类也不完全一样。

注意2:也叫做可见性重写。具体来说,重写时,子类方法不能降低父类方法的可见性,若父类方法是protected修饰,则子类重写的方法只能是protected、public修饰。

小汪:类的继承中,子类是无法继承父类private的属性和方法。

大榜:Java基础中,我还再次复习了类加载和对象创建的过程,之前我把类加载和对象创建搞混了,错误地认为类加载是将对象装载到内存中,并没有搞清楚类加载和对象创建地区别。

小汪:是啊,我知道类加载是将class字节码文件加载到内存中,也就是将类的相关信息加载到内存,此时是没有创建对象的。

大榜:你说得很对,但你提到的类的相关信息并没有说清楚?其实,一个类的信息包含下面2个部分:

1)静态变量、类初始化代码、静态方法; 2)实例变量、实例初始化代码、实例方法。

在Java中类是动态加载的,当第一次使用这个类的时候才会加载,加载一个类时,会查看父类是否已经加载,如果没有,则会先加载其父类。类加载只会对静态static修饰的部分进行加载,不会对实例部分进行加载。类加载的步骤如下:

a)在方法区中分配内存,用来保存类的信息;
b)给静态变量赋默认值;
c)加载父类,设置父子关系;
d)执行类初始化代码。

小汪:嗯嗯,类加载只会加载静态相关的部分,实例相关的部分应该是在对象创建的过程来完成的。

大榜:猜测得没问题。在类加载之后,如果碰到了"new"关键字,就到了对象创建的过程,比如new Child() 就是创建Child对象。对象创建的步骤包括:

a)分配内存;
b)给实例变量赋默认值;
c)执行实例初始化代码。

小汪:总结得很好,我理解一遍啊,类加载就是将静态相关的信息加载到内存,供后续创建类的对象来使用。对象创建,是根据类的信息创建类的实例对象。

5、Java基础之面向接口编程

大榜:看来你已经完全理解了类加载、对象创建的区别和联系。接下来,我们聊聊面向接口编程,面向接口编程关注的是接口,而不是具体类型,它是计算机程序的一种重要的思维方式。首先我们定义一个比较器接口MyComparable,它拥有对数据进行比较的能力。

package com.programming.logic.p5.p5_2;
​
/**
 * 比较器接口:拥有对数据进行比较的能力或行为。
 *
 * 总结:
 * 1)接口降低了耦合,提高了灵活性。使用接口的代码依赖于接口本身,而不是实现接口的具体类型,程序员可以根据情况替换接口的实现,而不影响接口的使用者。
 *
 * 2)接口和抽象类的区别和联系
 *  a)两者都不能创建对象;
 *  b)接口是100%的抽象,而抽象类中可以有抽象方法和具体方法。
 *  c)接口中不能定义实例变量,而抽象类中可以定义实例变量;
 *  d)一个类可以实现多个接口,但只能继承一个类;
 *  e)接口声明某种行为,抽象类提供默认实现(实现全部或部分方法),方便 子类 来实现接口。在Java API中,List接口和对应的AbstractList抽象类。
 *
 *
 * @author qinxubang
 * @Date 2022/5/15 17:31
 */
public interface MyComparable {
    int compareTo(Object other);
}

接着,我们定义一个CompPoint类,实现了MyComparable接口,重写了接口中的compareTo方法,代码如下:

@Override
    public int compareTo(Object other) {
        if (!(other instanceof CompPoint)) {
            throw new IllegalArgumentException();
        }
        // 强制转换
        CompPoint otherPoint = (CompPoint) other;
        double delta = distance() - otherPoint.distance();
        if (delta < 0) {
            return -1;
        } else if (delta > 0) {
            return 1;
        } else {
            return 0;
        }
​
    }

然后,我们定义比较器的工具类CompUtils,针对MyComparable接口来进行编程,而不是具体类型CompPoint编程,代码如下:

package com.programming.logic.p5.p5_2;

/**
 * 比较器的工具类:针对任何实现了MyComparable接口的数组进行操作。我们这是面向接口编程,而不是具体类型进行编程,是计算机程序的一种重要的思维方式。
 * CompUtils可以处理多种不同类型的对象,只要实现了MyComparable接口的数组都可以进行操作。
 *
 * @author qinxubang
 * @Date 2022/5/15 17:31
 */
public class CompUtils {

    public static Object max(MyComparable[] objs) {
        if (objs == null || objs.length <= 0) {
            return null;
        }

        MyComparable max = objs[0];
        // 遍历数组,获取最大值max
        for (int i = 1; i < objs.length; i++) {
            if (max.compareTo(objs[i]) < 0) {
                max = objs[i];
            }
        }
        return max;
    }

    /**
     * 选择排序算法,按照从小到达排列
     * @param objs
     */
    public static void sort(MyComparable[] objs) {
        for (int i = 0; i < objs.length; i++) {
            int min = i;
            for (int j = i + 1; j < objs.length; j++) {
                if (objs[j].compareTo(objs[min]) < 0) {
                    min = j;
                }
            }

            if (min != i) {
                MyComparable temp = objs[i];
                objs[i] = objs[min];
                objs[min] = temp;
            }

        }
    }

}

CompUtils可以处理多种不同类型的对象,只要实现了MyComparable接口的数组都可以进行操作。

小汪:是啊,这个CompUtils不仅可以对CompPoint具体类型进行处理,而且可以对任何实现了MyComparable接口的类型进行处理,难道这就是面向接口编程的强大之处吗?

大榜:我们来写一个测试类ComparableTest,来感受下面向接口编程的好处。

package com.programming.logic.p5.p5_2;

import java.util.Arrays;

/**
 * @author qinxubang
 * @Date 2022/5/15 17:38
 */
public class ComparableTest {

    public static void main(String[] args) {
//        MyComparable point1 = new CompPoint(2, 3);
//        MyComparable point2 = new CompPoint(3, 2);
//        System.out.println("比较两个点的值的大小:");
//        System.out.println(point1.compareTo(point2));

        CompPoint[] points = new CompPoint[]{
          new CompPoint(2, 3), new CompPoint(3, 4), new CompPoint(1, 2)
        };

        // CompUtils.max方法,是面向接口编程的,程序员只需要传入实现了MyComparable接口的数组,就可以获取数组中的最大值
        System.out.println("最大值:" + CompUtils.max(points));

        CompUtils.sort(points);
        System.out.println("按照从小到大,排序之后:" + Arrays.toString(points));
    }
}

CompUtils.max方法,是面向接口编程的,调用者只需要传入实现了MyComparable接口的数组,就可以获取数组中的最大值。

小汪:面向接口编程好处多多,我也可以定义一个compLine类,去实现MyComparable接口,重写compareTo方法,然后为了比较多个compLine的长度,我也可以去调用CompUtils.max方法,从而获取数组中的最大值。compLine类的代码:

package com.programming.logic.p5.p5_2;

import com.programming.logic.p3.p3_1_3.Point3;

/**
 * @author qinxubang
 * @Date 2022/5/17 9:12
 */
public class CompLine implements MyComparable {

    private Point3 startPoint;

    private Point3 endPoint;

    public CompLine() {}

    public CompLine(Point3 startPoint, Point3 endPoint) {
        this.startPoint = startPoint;
        this.endPoint = endPoint;
    }

    public double length() {
        // 参数校验
        if (startPoint != null && endPoint != null) {
            return startPoint.distance(endPoint);
        } else {
            System.out.println("传入的Point参数为null");
            return -1;
        }
    }

    /**
     * 比较两条线的长度
     * @param other
     * @return
     */
    @Override
    public int compareTo(Object other) {
        if (!(other instanceof CompLine)) {
            throw new IllegalArgumentException();
        }
        // 强制转换
        CompLine otherLine = (CompLine) other;

        double delta = length() - otherLine.length();
        if (delta < 0) {
            return -1;
        } else if (delta > 0) {
            return 1;
        } else {
            return 0;
        }
    }


    @Override
    public String toString() {
        return "CompLine{" +
                "startPoint=" + startPoint +
                ", endPoint=" + endPoint +
                '}';
    }
}

测试类CompLineTest,代码:

package com.programming.logic.p5.p5_2;

import com.programming.logic.p3.p3_1_3.Point3;

import java.util.Arrays;
import java.util.Random;

/**
 * @author qinxubang
 * @Date 2022/5/17 9:16
 */
public class CompLineTest {

    public static void main(String[] args) {
        CompLine[] compLines = new CompLine[3];
        Random random = new Random();

        // 初始化数组compLines
        for (int i = 0; i < compLines.length; i++) {
            Point3 startPoint = new Point3(i, i);
            Point3 endPoint = new Point3(i + random.nextInt(10), i);

            compLines[i] = new CompLine(startPoint, endPoint);
        }
        System.out.println("所有的线:" + Arrays.toString(compLines));

        System.out.println("最长的线:" + CompUtils.max(compLines));

    }
}

我们定义了一个compLines数组,用来存放多个compLine对象,为了比较多个线段的长度,我直接去调用榜哥的CompUtils.max方法,从而获取最长的线段。

输出结果如下:

所有的线:[CompLine{startPoint=Point3{x=0, y=0}, endPoint=Point3{x=1, y=0}}, CompLine{startPoint=Point3{x=1, y=1}, endPoint=Point3{x=4, y=1}}, CompLine{startPoint=Point3{x=2, y=2}, endPoint=Point3{x=2, y=2}}]
最长的线:CompLine{startPoint=Point3{x=1, y=1}, endPoint=Point3{x=4, y=1}}

大榜:学以致用,你小子厉害呀,将来是个好苗子!

6、查漏补缺

总结: 通过小汪和大榜的对话,我们对Java的部分基础进行了回炉学习,重点介绍了switch/case的语法陷阱,接着和大家一起回顾了封装、继承的概念,然后对类加载和创建对象的过程进行了对比分析,最后对面向接口编程,编写了案例实现。

在封装概念中,我们围绕Point类、Line进行了介绍,Point类表示数学上的一个平面上的坐标,Line类是由两个Point组成,表示一条线,围绕Point类讲解了封装的概念,Line类讲解了类的组合的思想。最后,以Point、Line类为基础,我们编写了MyComparable接口、CompUtils工具类,重点介绍了面向接口编程的强大之处。码云仓库地址:gitee.com/qinstudy/ja…

7、参考内容

《Java编程的逻辑》-马俊昌,第一部分(编程基础与二进制)、第2部分(面向对象)。