Java-2022最新入门教程-课程同步更新

Java基本说明与介绍

Java发展历程介绍

Java于1995年5月由Sun公司推出(高级程序设计语言),其可以运行多个平台(依赖于Jre运行环境,通过安装不同的Jre环境即可实现在不同环境下运行相同的Java程序)
Java分为JavaSE(J2SE)、JavaEE(J2EE)、JavaME(J2ME),Sun公司于2005年6月推出Java SE 6。

Java的主要特性

Java语言是简单的:
Java语言的语法与C语言和C++语言很接近,使得大多数程序员很容易学习和使用。另一方面,Java丢弃了C++中很少使用的、很难理解的、令人迷惑的那些特性,如操作符重载、多继承、自动的强制类型转换。特别地,Java语言不使用指针,而是引用。并提供了自动的废料收集,使得程序员不必为内存管理而担忧。
Java语言是面向对象的:
Java语言提供类、接口和继承等原语,为了简单起见,只支持类之间的单继承,但支持接口之间的多继承,并支持类与接口之间的实现机制(关键字为implements)。Java语言全面支持动态绑定,而C++语言只对虚函数使用动态绑定。总之,Java语言是一个纯的面向对象程序设计语言。
Java语言是分布式的:
Java语言支持Internet应用的开发,在基本的Java应用编程接口中有一个网络应用编程接口(java net),它提供了用于网络应用编程的类库,包括URL、URLConnection、Socket、ServerSocket等。Java的RMI(远程方法激活)机制也是开发分布式应用的重要手段。
Java语言是健壮的:
Java的强类型机制、异常处理、垃圾的自动收集等是Java程序健壮性的重要保证。对指针的丢弃是Java的明智选择。Java的安全检查机制使得Java更具健壮性。
Java语言是安全的:
Java通常被用在网络环境中,为此,Java提供了一个安全机制以防恶意代码的攻击。除了Java语言具有的许多安全特性以外,Java对通过网络下载的类具有一个安全防范机制(类ClassLoader),如分配不同的名字空间以防替代本地的同名类、字节代码检查,并提供安全管理机制(类SecurityManager)让Java应用设置安全哨兵。
Java语言是体系结构中立的:
Java程序(后缀为java的文件)在Java平台上被编译为体系结构中立的字节码格式(后缀为class的文件),然后可以在实现这个Java平台的任何系统中运行。这种途径适合于异构的网络环境和软件的分发。
Java语言是可移植的:
这种可移植性来源于体系结构中立性,另外,Java还严格规定了各个基本数据类型的长度。Java系统本身也具有很强的可移植性,Java编译器是用Java实现的,Java的运行环境是用ANSI C实现的。
Java语言是解释型的:
如前所述,Java程序在Java平台上被编译为字节码格式,然后可以在实现这个Java平台的任何系统中运行。在运行时,Java平台中的Java解释器对这些字节码进行解释执行,执行过程中需要的类在联接阶段被载入到运行环境中。
Java是高性能的:
与那些解释型的高级脚本语言相比,Java的确是高性能的。事实上,Java的运行速度随着JIT(Just-In-Time)编译器技术的发展越来越接近于C++。
Java语言是多线程的:
在Java语言中,线程是一种特殊的对象,它必须由Thread类或其子(孙)类来创建。通常有两种方法来创建线程:其一,使用型构为Thread(Runnable)的构造子将一个实现了Runnable接口的对象包装成一个线程,其二,从Thread类派生出子类并重写run方法,使用该子类创建的对象即为线程。值得注意的是Thread类已经实现了Runnable接口,因此,任何一个线程均有它的run方法,而run方法中包含了线程所要运行的代码。线程的活动由一组方法来控制。Java语言支持多个线程的同时执行,并提供多线程之间的同步机制(关键字为synchronized)。
Java语言是动态的:
Java语言的设计目标之一是适应于动态变化的环境。Java程序需要的类能够动态地被载入到运行环境,也可以通过网络来载入所需要的类。这也有利于软件的升级。另外,Java中的类有一个运行时刻的表示,能进行运行时刻的类型检查。

Java程序入门介绍

Java基础语法

一个Java程序可以认为是一系列对象的集合,而这些对象通过调用彼此的方法来协同工作。下面简要介绍下类、对象、方法和实例变量的概念。
对象:对象是类的一个实例,有状态和行为。例如,一条狗是一个对象,它的状态有:颜色、名字、品种;行为有:摇尾巴、叫、吃等。
类:类是一个模板,它描述一类对象的行为和状态。
方法:方法就是行为,一个类可以有很多方法。逻辑运算、数据修改以及所有动作都是在方法中完成的。
实例变量:每个对象都有独特的实例变量,对象的状态由这些实例变量的值决定。

编写Java程序时,应注意以下几点:

  • 大小写敏感:Java是大小写敏感的,这就意味着标识符Hello与hello是不同的。
  • 类名:对于所有的类来说,类名的首字母应该大写。如果类名由若干单词组成,那么每个单词的首字母应该大写,例如 MyFirstJavaClass 。
  • 方法名:所有的方法名都应该以小写字母开头。如果方法名含有若干单词,则后面的每个单词首字母大写。
  • 源文件名:源文件名必须和类名相同。当保存文件的时候,你应该使用类名作为文件名保存(切记Java是大小写敏感的),文件名的后缀为.java。(如果文件名和类名不相同则会导致编译错误)。
  • 主方法入口:所有的Java 程序由public static void main(String []args)方法开始执行

Java标识符

Java所有的组成部分都需要名字。类名、变量名以及方法名都被称为标识符。
关于Java标识符,有以下几点需要注意:
所有的标识符都应该以字母(A-Z或者a-z),美元符($)、或者下划线(_) 开始
首字符之后可以是任何字符的组合
关键字不能用作标识符
标识符是大小写敏感的
合法标识符举例:age、$salary、_value、__1_value
非法标识符举例:123abc、-salary

case

Java修饰符

像其他语言一样,Java可以使用修饰符来修饰类中方法和属性。主要有两类修饰符:
访问控制修饰符 : default, public , protected, private
非访问控制修饰符 : final, abstract, strictfp
在后面的章节中我们会深入讨论Java修饰符。

Java变量

Java中主要有如下几种类型的变量
局部变量:
类变量(静态变量)
成员变量(非静态变量)

Java关键字

关键字说明
abstract抽象方法,抽象类的修饰符
assert断言条件是否满足
boolean布尔数据类型
break跳出循环或者label代码段
byte8-bit 有符号数据类型
caseswitch语句的一个条件
catch和try搭配扑捉异常信息
char16-bit Unicode字符数据类型
class定义类
const未使用
continue不执行循环体剩余部分
defaultswitch语句中的默认分支
do循环语句,循环体至少会执行一次

等等等

第一个Java程序

package com.company;

/**
 * @author darryl
 */
public class Main {

    public static void main(String[] args) {
        // write your code here
        System.out.println("Hello java !");
    }
}

至此,完成了第一个Java程序的书写。我们将该文件命名为 Main.java,这里我将该文件放置于D盘D:\workspace\code\mayun\java-project-example-22020719\src\com\company,通过CMD使用命令行完成程序的执行。
从控制台进入源文件目录
我们输入命令:

javac .\Main.java
dir #查询当前路径下的所有文件信息

执行完毕后,可以查看到下面的信息,我们可以发现,当前目录下多了以.class为后缀的文件信息。
在这里插入图片描述
我们再执行以下命令

java .\Main.class

可以看到,Java程序被正常的执行完成。实际上,完成这个操作我们可以借助于功能更加强大的IDE工具-IDEA或者Eclipese等,它们可以可视化的完成Java程序的执行过程,使程序执行更加的简单快捷。
在这里插入图片描述

Java基础类型

Java基础类型
在Java中,基本数据类型有八种,它们分别是 byte,int,float,double,boolean,long,char,short,接下来用表格的方式进行分别介绍:

数据类型最小值最大值默认值描述案例
byte-128(-2^7)127(2^7-1)0byte 数据类型是8位、有符号的,以二进制补码表示的整数byte a = 100;
short-32768(-2^15)32767(2^15 - 1)0short 数据类型是 16 位、有符号的以二进制补码表示的整数short s = 1000;
int-2,147,483,648(-2^31)2,147,483,647(2^31 - 1)0int 数据类型是32位、有符号的以二进制补码表示的整数int a = 100;
long-9,223,372,036,854,775,808(-2^63)9,223,372,036,854,775,807(2^63 -1)0Llong 数据类型是 64 位、有符号的以二进制补码表示的整数long a = 100L;
char\u0000(0)\uffff(65535)char类型是一个单一的 16 位 Unicode 字符char a = ‘A’;
float3.402823e+381.401298e-450.0ffloat 数据类型是单精度、32位、符合IEEE 754标准的浮点数float a = 234.5f;
double1.797693e+3084.9000000e-3240.0ddouble 数据类型是双精度、64 位、符合IEEE 754标准的浮点数double a = 123.4;
boolean0boolean数据类型表示一位的信息;只有两个取值:true 和 falseboolean a = true;
package com.company;

/**
 * @author darryl
 * @date 2022/7/19 14:52
 * Java基础类型介绍
 */
public class BaseType {

    /**
     * 我是程序执行入口
     *
     * @param args 默认参数
     */
    public static void main(String[] args) {
        //在Java中,基本数据类型有8种,且听我一一介绍

        boolean theBoolean = true;
        //boolean 类型只有两个值,true or false
        System.out.println("这是boolean类型的参数 theBoolean的值为:" + theBoolean);

        byte theByte = 100;
        //byte 类型是8位有符号数,可以表示 -2^7 到 2^7-1 的数值 即 1111 1111  -> 0111 1111 默认值为0
        System.out.println("这是byte类型的参数 theByte的值为:" + theByte + "最大值:" + Byte.MAX_VALUE + " 最小值:" + Byte.MIN_VALUE);

        short theShort = 10000;
        //short 类型可以表示比 byte 更多的数据,范围 -2^15 到 2^15 - 1
        System.out.println("这是short类型的参数 theShort的值为:" + theShort + "最大值:" + Short.MAX_VALUE + " 最小值:" + Short.MIN_VALUE);

        char theChar = 'A';
        //char类型是一个单一的 16 位 Unicode 字符,其可以表示 \u0000(0) 到 \uffff(65535)
        System.out.println("这是char类型的参数 theChar的值为:" + theChar);

        int theInt = 1000000000;
        //int 类型可以表示更大范围的数据, -2^31 到 2^31 - 1
        System.out.println("这是int类型的参数 theInt的值为:" + theInt + "最大值:" + Integer.MAX_VALUE + " 最小值:" + Integer.MIN_VALUE);

        long theLong = 1000000000000000000L;
        //long 数据类型是 64 位、有符号的以二进制补码表示的整数,可以表示 -2^63 到 2^63 -1 范围内的数值,赋值时值后面需要添加 L 或者 l
        System.out.println("这是long类型的参数 theLong的值为:" + theLong + "最大值:" + Long.MAX_VALUE + " 最小值:" + Long.MIN_VALUE);

        float theFloat = 1.0F;
        //float 数据类型是单精度、32位、符合IEEE 754标准的浮点数,其表示范围 3.402823e+38 到 1.401298e-45 ,赋值时值后面需要添加 F 或者 f
        System.out.println("这是float类型的参数 theFloat的值为:" + theFloat + "最大值:" + Float.MAX_VALUE + " 最小值:" + Float.MIN_VALUE);

        double theDouble = 1.00;
        //double 类型表示范围更广,1.797693e+308 到 4.9000000e-324 ,数据类型是双精度、64 位、符合IEEE 754标准的浮点数
        System.out.println("这是double类型的参数 theDouble的值为:" + theDouble + "最大值:" + Double.MAX_VALUE + " 最小值:" + Double.MIN_VALUE);

        //程序中使用的 Byte、Short、Integer、Long、Float、Double均是基本类型的包装类,这个后续会介绍到,当前使用的均是该包装类的基础方法即获取最大最小值

        //Java中的类型可以进行相互转换,和C语言一样,分为自动转换与强制转换,从小范围到大范围可以自动转换,大范围到小范围需要强制指定
        // byte -> short -> int -> long -> float -> double
        theShort = theByte;
        theInt = theShort;
        theLong = theInt;
        theFloat = theLong;
        theDouble = theFloat;
        System.out.println("Java各类型自动转换后,最终输出结果为 theDouble:" + theDouble);

        //boolean 和 char 类型不参与转换过程 但 char 可以转化为 int
        theInt = theChar;
        System.out.println("char 类型转化为 int 类型后, theInt:" + theInt + " 其输出的是指定字符的Unicode码值");

        //当需要强制转换时,需要在参数前添加括号并指定目标类型
        theInt = (int) theDouble;
        System.out.println("强制转换不推荐,容易丢失数据精度,转换之后 theInt:" + theInt);
    }
}

系统输入与输出

演示代码

package com.yihaoclub.java;

import java.util.Scanner;

/**
 * 控制台输入输出演示
 */
public class ConsoleInputAndPrint {

    /**
     * 主函数入口
     *
     * @param args 参数
     */
    public static void main(String[] args) {
        //1.让用户输入目标字符串
        String inputString = inputString();
        //2.打印用户输入的目标
        print(inputString);
    }

    /**
     * 打印输出目标参数
     * 输出格式 正常输出
     *
     * @param printContent 代输出参数
     */
    public static void print(String printContent) {
        System.out.print(printContent);
    }

    /**
     * 从控制台输入一个字符串并返回该字符串
     *
     * @return 控制台输入的字符串
     */
    public static String inputString() {
        print("请输入您要展示的内容:");
        Scanner scanner = new Scanner(System.in);
        //nextLine 与 next 的区别
        return scanner.next();
    }
}

Java数据类型转换

这里首先介绍Java中常见的7种基本类型之间的转换关系,下图列举出这7种基本类型之间的一个大小关系:
Java数据范围从小到大图

public static void main(String[] args) {
    int a = 100;
    //定义一个小数
    double b = 10.0;
    //数据类型转换
    //a -> int 类型 转换成别的一种类型  float  double  short   byte

    //数据范围 从小范围到大范围的一个过程
    // byte  --->  short   --->   int --->  long   ---> float  ----> double
    //自动转换 由Java虚拟机自动的去提升我们的类型范围 int --> long 完全不需要指定
    long longLong = a;
    System.out.println("自动转换 int 转 long 转换结果:" + longLong);
    double decimal = a;
    System.out.println("自动转换 int 转 double 转换结果:" + decimal);
    //char 转 数值类型
    char ch = 'a';
    System.out.println("ch = " + ch);
    int chInt = ch;
    System.out.println("char 转 int 运行结果:" + chInt);

    //大范围转换为小范围  强制转换
    int intA = 100;
    //强制转换的时候,只需要指定好目标类型    目标类型 目标类型变量 = (目标类型)待转换目标;
    short shortA = (short) intA;
}

数据范围从小到大排布。

类型自动转换

把一个表示数据范围小的数值或者变量赋值给另一个表示数据范围大的变量。这种转换方式是自动的,如 short --> int ,这种转换是自动的,只需要直接书写出来即可

public static void main(String[] args) {
     System.out.println("演示Java类型自动转换");
     short count = 1;
     int countInt = count;
     System.out.println("自动类型转换从小范围转换到大范围,只需要直接赋值即可,会自动将值转换");
}

这种转换方式需要值得注意的是,char类型转换为数值类型是通过ASCII码值进行转换的。

类型强制转换

强制类型转换的概念与它相反,强制类型转换是大范围的数据类型转为小范围的数据类型时进行强制类型转换,需要自己去操作。
强制类型转换格式:

目标数据类型 目标数据类型变量名 = (目标数据类型)待转换的类型变量或值;

public static void main(String[] args) {
     System.out.println("演示Java类型强制转换");
     int count = 1;
     short countShort = (int)count;
}

这里我们来看一个比较有意思的案例:

public static void main(String[] args) {

    short shortA = 4;

    short shortB = 5;

    //首先,用两个不超出short范围的数值直接相加,可以看到编译器是正常的
    short shortC = 4 + 5;

    //这时候后,我们使用上面定义的shortA再来进行一次相加,可以看到编译器报错了
    shortC = shortA + 5;
    
    //为什么上面这段代码会提示我们提供的值是个 int 类型?
    //我用shortA+shortB 呢
    shortC = shortA + shortB;
    //还是报了相同的错误
}

上面报错行的截图
Short类型求和转换为int类型
这里面呢,要引入到Java诞生的时代,首先,byte类型能表示的数据范围太小了,从硬件的角度讲,为较小的类型引入专用的算术逻辑单元不值得付出努力,因为需要引入额外的晶体管,而尽管这样做了,它仍然只能在一个时钟周期内执行一次加法,所以并不是特别的有必要。而JVM设计时,当时机器以32位为主,适合32位的int类型,没有设计byte类型的加法,只支持int类型的加法,所以,两个 byte变量在求和时会将byte自动转换成 int之后再求和。

那么为什么我们使用 shortA + 5 这样的方式也不行呢?
首先,shortA是一个byte类型的变量,求和时,shortA首先会被变更为 int类型,相当于 int + 常量值,这时候,编译器会自动获取最大的数据类型 int 来组装结果值,所以这个结果类型也成了 int .

最后,为什么 4+5可以被byte类型接收到呢?
这两个值属于常量,在被编译成字节码时,就已经检查了他们求和的类型,发现并没有超出byte类型的数值范围,所以可以被byte类型接收到。

Java变量

变量我们可以理解为存储数值(字符、数据等)的容器,在Java中,有不同类型的变量,这点在Java基础类型里面已经有讲解过,这里要引进一个不属于基础类型的类型–引用类型:
String
String 类型可以存储文本,例如“hello”。字符串值用双引号括起来
Java中变量的定义方式如下:

type variable = value;

其中type是Java的类型之一(基本类型和引用类型,如int或String),variable 是变量的名称(如:name),等号用于将值value分配给定义的变量,例如:

int code = 11;
String name = “张三”;

这里演示一个定义一个String类型的变量并赋值、修改值的过程

public static void main(String[] args) {
        String name = "张三";
        System.out.println("name 变量的值为: name=" + name);
        name = "李四";
        System.out.println("name 变量的值为: name=" + name);
    }

变量定义运行结果
我们来分析下这段代码的执行过程,首先我们定义了一个变量 name(这里暂时不考虑常量池的问题),是一个String(引用类型)的变量,这时候JVM在内存中为变量 name 分配了一个存储单元,填入值 “张三”。

你也可以定义一个变量,等到使用的时候再赋值,比如:

public static void main(String[] args) {
        String name ;
        name = "张三";
        System.out.println("name 变量的值为: name=" + name);
    }

但是变量如果没有赋值就使用,那么编译器会提醒你使用了未初始化的变量
编译器提醒使用了未初始化的变量
如果你需要定义一个变量,不想让该变量被修改,那么可以添加关键字 final ,当你尝试修改final修饰的变量时,编译器会提示你不能修改被final修饰的变量:
在这里插入图片描述
final修饰的变量只可以赋值一次,即你可以先定义该变量,等到使用时再赋值。

public static void main(String[] args) {
        final String name ;
        name = "张三";
        System.out.println("name 变量的值为: name=" + name);
}

案例-交换两个变量的值

public static void main(String[] args) {
        String name = "张三";
        String name_2 = "李四";
        String temp;
        System.out.println("交换前,name 变量的值为: name=" + name + ",name_2 变量的值为:name_2="+name_2);
        //交换两个变量的值,可以通过temp临时变量存储过程值
        //1.将name值赋给temp变量,那么此时 张三 已经赋值给 temp 即 temp="张三"
        temp = name;
        //2.将name_2 的值赋给 name变量,此时,name="李四"
        name = name_2;
        //3.将temp变量的值赋给 name_2,此时name_2 = "张三",完成两变量交换值
        name_2 = temp;
        System.out.println("交换后,name 变量的值为: name=" + name + ",name_2 变量的值为:name_2="+name_2);
}

局部变量

局部变量用书面一点的语言来讲,就是成员方法里面定义的变量。
用简单点的话来说,局部变量就是定义在我们的方法内部(函数内部)的变量,上文中定义的所有的变量都是局部变量。这里还是使用一个案例来说明局部变量的定义以及局部变量的作用域(变量可以被访问到的范围)。

public static void main(String[] args) {
		//这里定义了一个局部变量(main方法内部)name,它在整个main方法内都可以被访问到
        String name = "张三";
        System.out.println("name 变量的值为: name=" + name);
        {
        	//请注意,同一作用域下的变量名称不能重复,大括号内部的作用域小于大括号外部
        	//这里定义了一个内部作用域下的变量,它只能在该大括号内部才能被访问到
        	String name_2 = "李四";
        	System.out.println("name_2 变量的值为: name_2=" + name);
        }
        {
        	//同级作用域之间的变量定义不受干扰,可以定义重复的名称
        	String name_2 = "神奇吧,这个名称跟上一个大括号内部名称可以重复";
        	System.out.println("name_2 变量的值为: name_2=" + name);
        }
        //在这里已经无法访问到之前在大括号内部定义的局部变量,但是还可以访问到main方法内部定义的name变量
        System.out.println("name 变量的值为: name=" + name);
}

这里再对局部变量进行一个总结:

1.局部变量是只有在特定的过程或函数中可以访问的变量
2.局部变量退出方法即销毁
3.访问修饰符(private,public等)不能用于局部变量

类变量(静态变量)

类变量(静态变量)是类(class)中独立于方法之外的变量,用static来进行修饰,类变量在整个实例化的对象中是公用的。

这里我们还是使用案例来进行介绍:

public class Student {

    /**
     * 定义一个类变量,这里作为一个计数器(在案例中的意义)
     */
    static int count = 0;

    public static void main(String[] args) {
        addHistorySayCount(10);
        sayHello();
        sayHello();
        addHistorySayCount(2);
        sayHello();
    }

    /**
     * 计数并打印count方法执行情况
     */
    static void sayHello() {
        //这里给count变量加1,标识sayHello方法执行多了一次
        count = count + 1;
        System.out.println("学生说Hello了,当前是第 " + count + " 次");
    }

    /**
     * 执行加入历史执行数量(随机指定即可,用作演示全局变量类内部方法均可访问)
     *
     * @param historyCount 历史计数次数
     */
    static void addHistorySayCount(int historyCount) {
        count = count + historyCount;
        System.out.println("系统执行时,加入历史执行次数,执行后count =" + count);
    }
}

在该方法中,类(class)Student的类变量count被类中 sayHello、addHistorySayCount两方法所共享,并且其值在类中一直保留。

Java运算符

算数运算符

我们将 加/减/乘/除/求余数 这些运算符称之为 算数运算符,他们的具体表现形式如下:

运算符作用说明
+求两数之和
-求两数之差
*求两数乘积
/除法求两数之商
%取余获取两数据的余数
~按位取反将所得数按位取反,暂不做要求

这里首先使用一个例子来讲解下这几个运算符的使用方法:

public static void main(String[] args) {
    System.out.println("这里演示 + 加 算术运算符 的使用");
    //这里为了便于举例,类型名称比较简洁,后期程序中应该避免这种取名
    int a = 11;
    int b = 6;
    //这里为什么需要使用括号括起来呢,因为在所运算时,这些运算符的运算顺序是相同的,
    // Java中 "aaa" + 4 = "aaa4" 就是说字符串求和会直接变更为字符串,这里后期我们会详细展开说明
    System.out.println("我们求 a + b = " + (a + b));
    System.out.println("-----------------我是分隔符------------------");
    System.out.println("这里演示 - 减 算术运算符 的使用");
    System.out.println("我们求 a - b = " + (a - b));
    System.out.println("-----------------我是分隔符------------------");
    System.out.println("这里演示 * 乘 算术运算符 的使用");
    System.out.println("我们求 a * b = " + (a * b));
    System.out.println("-----------------我是分隔符------------------");
    System.out.println("这里演示 / 除 算术运算符 的使用");
    System.out.println("我们求 a / b = " + (a / b));
    System.out.println("-----------------我是分隔符------------------");
    System.out.println("这里演示 % 求余 算术运算符 的使用");
    System.out.println("我们求 a % b = " + (a % b));
}

这里再介绍说明下除法和求余两个运算符的使用:

int a = 11;
int b = 5;
int c = a / b;
System.out.println("c = " + c);

那么这里我们的输出结果应该是怎样的呢? c = 2.2?我们来看一下具体的输出结果:
运算结果
c=2,这运算结果看似不太对?
这里就得说明一下运算方式了,我们运算符在进行操作时,整数操作只能得到整数,要想得到小数,必须有浮点数参与运算。那我们要得到一个比较准确的值需要怎么去操作呢?我们将其中一个值定义为double即可

int a = 11;
float b = 5.0;
float c = a / b;
System.out.println("c = " + c);

运算结果
我们再来说明下求余数的 % 算数符号,他的运算结果实际上是这样的:

int a = 11;
float b = 5.0;
//我们使用 a / b 时,我们最终的结果应该是 商 2 余数 1
//求余运算就是获取我们的余数

算术表达式中包含不同的基本数据类型的值的时候,整个算术表达式的类型会自动进行提升。

提升规则:

1.byte类型,short类型和char类型将被提升到int类型,不管是否有其他类型参与运算。
2.整个表达式的类型自动提升到与表达式中最高等级的操作数相同的类型
3.等级顺序:byte,short,char --> int --> long --> float --> double

正是由于上述原因,所以在程序开发中我们很少使用byte或者short类型定义整数。也很少会使用char类型定义字符,而使用字符串类型,更不会使用char类型做算术运算。当“+”操作中出现字符串时,这个”+”是字符串连接符,而不是算术运算。在”+”操作中,如果出现了字符串,就是连接运算符,否则就是算术运算。当连续进行“+”操作时,从左到右逐个执行。

赋值运算符

赋值运算符的作用是将一个表达式的值赋给左边,左边必须是可修改的,不能是常量。下面列举下具体的赋值运算符:

运算符作用说明
=赋值将等号右侧的结果赋值给等号左侧
+=求和等于a += b; 相当于 a=a+b;
-=求差等于a -= b; 相当于 a = a-b;
*=求乘积等于a *= b;相当于 a = a*b;
/=求商等于a /= b; 相当于 a= a/b;
%=求余等于a %= b; 相当于 a = a%b;

这里还是用案例来说明这几个运算符的使用方法:

public static void main(String[] args) {
    int a = 11;
    int b = 5;
    System.out.println("赋值运算符 = 的案例:将等号右侧的值赋值给左侧");
    int c = a + 12;
    System.out.println("a + 12 运算结果 c = "+c);
    System.out.println("赋值运算符 += 的案例");
    //b += a 形如 b = b+a;
    b += a;
    System.out.println(" b += a 运算结果 b = "+b);
    System.out.println("赋值运算符 -= 的案例");
    //b -= a 形如 b = b-a;
    b -= a;
    System.out.println("b -= a 运算结果 b = "+b);
    System.out.println("赋值运算符 *= 的案例");
    //b *= a 形如 b = b*a;
    b *= a;
    System.out.println("b *= a 运算结果 b = "+b);
    System.out.println("赋值运算符 /= 的案例");
    //b /= a 形如 b = b/a;
    b /= a;
    System.out.println("b /= a 运算结果 b = "+b);
    System.out.println("赋值运算符 %= 的案例");
    //a %= b 形如 a = a%b;
    a %= b;
    System.out.println("a %= b 运算结果 a = "+a);
}

逻辑运算符

运算符作用说明
&&逻辑与a&&b,a和b都是 true,那么结果为 true,否则为 false
||逻辑或a||b; a和b 都为 false,结果为false,否则为 true
&按位与对左右两值的二进制值进行与运算
|按位或对左右两值的二进制值进行或运算
^逻辑异或a^b; a和b结果不同为 true,否则为 false
!逻辑非结果和 a 的结果正好相反

运用案例:

public static void main(String[] args) {
    boolean a = true;
    boolean b = false;
    System.out.println(" a = " + a + " ;b= " + b + " ;a && b =" + (a && b));
    System.out.println(" a = " + a + " ;b= " + b + " ;a || b =" + (a || b));
    System.out.println(" a = " + a + " ;b= " + b + " ;a ^ b =" + (a ^ b));
    System.out.println(" a = " + a + " ;!b =" + (!b));
    int c = 10;
    int d = 8;
    //二进制运算 与运算 或运算
    System.out.println(" c = " + c + " ;d= " + d + " ;c & d =" + (c & d));
    System.out.println(" c = " + c + " ;d= " + d + " ;c | d =" + (c | d));
}

执行结果

关系运算符

运算符作用说明
<前值小于后值满足条件结果为 true 否则为false
>前值大于后值满足条件结果为 true 否则为false
==比较两值是否相等满足条件结果为 true 否则为false
!=两值是否不等满足条件结果为 true 否则为false
>=前值是否大于或等于后值满足条件结果为 true 否则为false
<=前值是否小于或等于后值满足条件结果为 true 否则为false
public static void main(String[] args) {
    int a = 10;
    int b = 20;
    System.out.println("a < b 的运行结果为 : " + (a < b));
    System.out.println("a > b 的运行结果为 : " + (a > b));
    System.out.println("a <= b 的运行结果为 : " + (a <= b));
    System.out.println("a >= b 的运行结果为 : " + (a >= b));
    System.out.println("a == b 的运行结果为 : " + (a <= b));
    System.out.println("a != b 的运行结果为 : " + (a >= b));
}

运行结果

自增自减运算符

运算符作用说明
++自增符分为b++和++b两种
自减符分为b–和–b两种

案例说明:

public static void main(String[] args) {
    int a = 10;
    // ++a 的情况,赋值时首先 a自增,再赋值给b
    int b = ++a;
    System.out.println("执行 b = ++a 之后,b = " + b + " ; a = " + a);
    // a++ 的情况,赋值时,先将a的值赋给b,然后再自增
    b = a++;
    System.out.println("执行 b = a++ 之后,b = " + b + " ; a = " + a);
    //自增自减也可以不赋值,均表示自增1
    a++;
    System.out.println("执行 a++ 之后,b = " + b + " ; a = " + a);
}

执行结果

位运算符

运算符作用说明
<<左移
>>右移
>>>无符号右移运算符暂不要求
public static void main(String[] args) {
	//以32位int类型举例
    int a = 10;
    //a二进制 0000 0000 0000 0000 0000 0000 0000 1010
    int b = a << 1;
    //       0000 0000 0000 0000 0000 0000 0001 0100  = 20
    System.out.println(b);

    //右移符号 >>
    //       0000 0000 0000 0000 0000 0000 0000 0101  = 5
    b = a >> 1;
    System.out.println(b);

    //       0000 0000 0000 0000 0000 0000 0000 0010  = 2
    b = b >> 1;
    System.out.println(b);
    //       0000 0000 0000 0000 0000 0000 0000 0001  = 1
    b = b >> 1;
    System.out.println(b);
    //       0000 0000 0000 0000 0000 0000 0000 0000  = 0
    b = b >> 1;
    System.out.println(b);
}

正数左移右移实际上就是在操作数的二进制数据上向左右移位,并分别补0
我们再来看看负数的左移右移运算(tips:二进制首位表示符号位,0为正,1为负):

public static void main(String[] args) {
    int a = -10;
    //a二进制 1000 0000 0000 0000 0000 0000 0000 1010
    int b = a << 1;
    //       1000 0000 0000 0000 0000 0000 0001 0100  = 20
    System.out.println(b);

    //       1000 0000 0000 0000 0000 0000 0010 1000  = 40
    b = b << 1;
    System.out.println(b);
}

执行结果
最终我们其实可以看出来,在进行位运算时,符号位不变,其它正常左移右移

条件运算符

A>B?A:B
条件运算符实际上可以简单的拆分下:

//条件运算符如下:
boolean bool = A>B?A:B;
//拆分之后如下:
if(A>B){
	bool = A;
}else{
	bool =B;
}

里面用到了条件判断uf else 语句,这个后期我们也还会再介绍一下。
分析该表达式,实际上就是当 A>B 的结果为 true时,将 ‘:’ 前面的值赋给对应变量,否则将后面的值赋给对应变量,我们可以用案例来看一下:

public static void main(String[] args) {
    int a = 3;
    int b = 4;
    int result = a > b ? a : b;
    System.out.println("result =" + result);
}

执行结果
这里的 A>B表示的不是说仅仅只是这种格式的表达式,只要是最终运行结果是 true 或者 false的逻辑表达式均可。

运算符优先级

优先级运算符说明
1. [] () {} , ;分隔符
2++ – ~ !单目运算符
3(type)强制类型转换运算符
4* / %乘法、除法、求余
5+ -加法、减法
6<< >> >>>移位运算符
7< <= > >= instanceof关系运算符
8== !=等价运算符
9&按位与
10^按位异或
11|按位或
12&&逻辑与
13||逻辑或
14? :三目运算符
15= += -= *= /= &== ^= %= <<= >>= >>>=

Java注释

Java注释分为单行注释和多行注释,这里简单说明下,Java代码在进行注释时,不建议直接将注释打在对应行后面,这样代码可读性不高,注释加在对应行上面即可。方法注释一定要写明方法作用,需要传递的参数(后期甚至需要指明必需还是非必需等),返回结果的说明。

Java方法

java方法:(method)是将具有独立功能的代码块组织成为一个整体,使其具有特殊功能的代码集,并且能实现达到代码复用的效果,java方法包含于类或对象中(在面向对象的时候我们就知道,其实类和对象是一个东西,只不过类是作为模板,而对象是根据这个模板具体创建出来的东西而已,所以这里可以解释为方法可以存在于类或对象中),java方法在程序中被创建,在其他地方被引用

设计方法的原则:

方法的本意是功能块,就是实现某个功能的语句块的集合,我们设计方法的时候,最好保持方法的原子性,就是一个方法只完成一个功能,这样利于我们后期的扩展!

方法定义:
方法必须先创建才可以使用,该过程称为方法定义

格式: 
public static 返回值类型 方法名(参数){
	方法体;
	return 数据;
}

public static 都是修饰符,目前先记住这个格式,返回值类型: 方法操作完毕之后返回的数据的数据类型
在这里插入图片描述
如果方法操作完毕,没有数据返回,这里写void,而且方法体中一般不写return方法名:调用方法时候使用的标识
参数: 由数据类型和变量名组成,多个参数之间用逗号隔开
方法体:完成功能的代码块
return:如果方法操作完毕,有数据返回,用于把数据返回给调用者

方法调用:
方法创建后并不是直接运行的,需要手动使用后才执行,该过程称为方法调用

格式: 方法名();

注意:方法必须在main方法里调用运行,因为main方法是程序的入口方法,你不调用在main方法里,你就没有程序运行,而其他方法与方法之间可以相互调用使用
方法没有被调用的时候,都在方法区中的字节码文件 .class中存储,调用时JVM会先自动识别mian方法,首先将main方法进行压栈,然后依次将需要被调用的方法像子弹夹一样进行压栈,而当这些方法或程序使用完之后就会进行弹栈,弹出栈内存

方法重载:
在同一个类中,可不可以存在同名的方法?
方法名相同,参数也完全相同,称为方法的重复定义,是一种冲突性的错误
调用方法的时候,java虚拟机会通过参数的不同来区分同名的方法
方法重载:在同一个类中,定义了多个同名的方法,但每个方法具有不同的参数类型或者参数个数,这些同名的方法,就构成了重载关系
注意:识别方法之间是否是重载关系,只看方法名和参数,跟返回值无关,就相当于你一个是void,一个是int,跟是否重载没有关系
方法重载的好处:不用记忆过多繁琐的方法名字

流程控制

分支

在这一个小模块中,我会介绍在Java中作条件判断的语句使用方法与相关的注意事项。这里首先介绍下什么是条件语句。

条件语句可以给定一个判断条件,并在程序执行过程中判断该条件是否成立,根据判断结果执行不同的操作,从而改变代码的执行顺序,实现更多的功能
写程序时,常常需要指明两条或更多的执行路径,而在程序执行时,允许选择其中一条路径,或者说当给定条件成立时,则执行其中某语句。在高级语言中,一般都要有条件语句

Java作为高级语言,条件语句也是必不可少的一部分。这里直接使用代码和注释先对条件语句进行介绍:

public static void main(String[] args) {
     int codeA = 12;
     int codeB = 13;
     //在Java中,条件语句的结构和C相同,都是if,else组成,使用方式也和C语言类似
     if (codeA == codeB) {
         System.out.println("codeA == codeB");
     }
     //上面的结构为纯if结构,在程序执行过程中,if内的语句为true时,执行if下包含的程序块,否则跳过该程序块
     if (codeA == codeB) {
        System.out.println("codeA == codeB");
     } else {
        System.out.println("codeA != codeB");
     }
     //上面的结构为if,else结构,当else上对应的if内语句为false时,会执行else中包含的语句
     //这里建议对各程序块使用{}进行包裹(包括只有一条语句),这样语句逻辑不易出错
     if (codeA == codeB) {
        System.out.println("codeA != codeB");
     } else if (codeA > codeB) {
        System.out.println("codeA > codeB");
     } else {
        System.out.println("codeA < codeB");
     }
     //在这里引入了else if 结构,可以对多种结果进行判别而分别处理
}

通过上面的代码,实际上我们可以看出,总共有三种结构,接下来重点阐述这三种结构:

if结构

仅有关键字if组成,其结构最为简单

if(条件表达式)
 	语句1;

if-else结构

该结构是比较常见的结构,其基本格式如下:

if(条件表达式)
 	语句1;
else
	语句2;

在执行该判断语句前,都是先执行了条件表达式的语句,条件表达式的返回结果必须是布尔值(boolean),根据条件表达式的返回,如果是true,那么就执行语句1的内容,如果是false就执行else后面的语句2

if- else if- else结构

该结构可以对执行情况进行更加详细的划分

if(条件表达式)
 	语句1;
else if(条件表达式)
	语句2;
	...
else
	语句3;

这样的语句在我们以后的编程中会经常用到,判断的过程是从上往下的判断条件表达式,如果第一个返回的是false就会判断第二个,依次类推,但是如果其中一个返回了true,那么就会执行后面的语句,然后整个else-if语句就会退出,后面如果还有else-if语句也不会在去判断执行的了。我们常常也会在最后面添加一个else语句,当然这也是可选的,这样的效果就是如果上面的所有的if判断都是false,那么就会执行else后面的语句

条件判断中还有一种结构,我也放在这一个模块中进行讲述,首先引入一个案例:

public static void main(String[] args) {
    int codeA = 100;
    int codeB = 200;
    System.out.println("codeA与codeB两者之间的最大值为:"+returnTheMaxCode(codeA,codeB));
}
public static int returnTheMaxCode(int codeA, int codeB) {
    return codeA > codeB ? codeA : codeB;
}

上面的案例实际上逻辑非常简单,就是调用了一个方法(函数),然后获取两数之间的较大值,而在方法中,使用了 条件语句 ? 语句1 : 语句2 这样一种结构,这种结构实际上使用起来也是十分方便,通过判断条件语句是否为真,如果为真那么就执行语句1,如果不为真,那么执行语句2。这种结构在实际运用中还是比较常见的,可以缩减代码数量,提升代码可读性。

switch语句
这里还是首先介绍一个代码案例:

public static void main(String[] args) {
        int code = 2;
        switch (code) {
            case 1: {
                //执行语句
                System.out.println("code = 1");
                break;
            }
            case 2: {
                //执行语句
                System.out.println("code = 2");
                break;
            }
            default: {
                //执行语句
                System.out.println("code != 2 && code != 1");
            }
        }
}

这段代码执行的逻辑相对来讲是很简单的,就是判别code的值,然后根据不同的值执行不同的语句,看完这些代码,接下来引出switch语句,switch语句,判断一个变量与一系列值中某个值是否相等,每个值称为一个分支,其语法格式如以下:

switch(expression){
    case value :
       //语句
       break; //可选
    case value :
       //语句
       break; //可选
    //你可以有任意数量的case语句
    default : //可选
       //语句
}

这里建议每一个case以及default后面的语句均加上代码块{},这样代码结构更加清晰。switch语句的相关注意事项如下:

  1. switch 语句中的变量类型可以是: byte、short、int 或者 char。从 Java SE 7 开始,switch 支持字符串 String 类型了,同时 case 标签必须为字符串常量或字面量。
  2. switch 语句可以拥有多个 case 语句。每个 case 后面跟一个要比较的值和冒号。
  3. case 语句中的值的数据类型必须与变量的数据类型相同,而且只能是常量或者字面常量。
  4. 当变量的值与 case 语句的值相等时,那么 case 语句之后的语句开始执行,直到 break 语句出现才会跳出 switch 语句。
  5. 当遇到 break 语句时,switch 语句终止。程序跳转到 switch 语句后面的语句执行。case 语句不必须要包含 break 语句。如果没有 break 语句出现,程序会继续执行下一条 case 语句,直到出现 break 语句。
  6. switch 语句可以包含一个 default 分支,该分支一般是 switch 语句的最后一个分支(可以在任何位置,但建议在最后一个)。default 在没有 case 语句的值和变量值相等的时候执行。default 分支不需要 break 语句。

这里需要特别注意,如果 case 语句块中没有 break 语句时,JVM 并不会顺序输出每一个 case 对应的返回值,而是继续匹配,匹配不成功则返回默认 case。当匹配成功时,会继续执行直到遇到 break 语句为止。也就是说,如果当前匹配成功的 case 语句块没有 break 语句,则从当前 case 开始,后续所有 case 的值都会输出,如果后续的 case 语句块有 break 语句则会跳出判断。

循环

在Java中,循环结构主要有三种结构,这里首先给出这三种结构:

while循环

while( 布尔表达式 ) {
  //循环内容
}

while循环,只要布尔表达式值为真,那么就会一直执行下去,下面介绍一个代码案例:

public static void main(String[] args) {
        int code = 12;
        while(code > 1){
            System.out.println("code == 1");
            code --;
        }
}

do while循环

do {
    //代码语句
}while(布尔表达式);

对于 while 语句而言,如果不满足条件,则不能进入循环。但有时候我们需要即使不满足条件,也至少执行一次。do…while 循环和 while 循环相似,不同的是,do…while 循环至少会执行一次。下面介绍一个代码案例:

public static void main(String[] args) {
        int code = 12;
        do{
            System.out.println("code == 1");
            code --;
        } while(code > 1);
}

for循环

for(初始化; 布尔表达式; 更新) {
    //代码语句
}

关于 for 循环有以下几点说明:

最先执行初始化步骤。可以声明一种类型,但可初始化一个或多个循环控制变量,也可以是空语句。
然后,检测布尔表达式的值。如果为 true,循环体被执行。如果为false,循环终止,开始执行循环体后面的语句。
执行一次循环后,更新循环控制变量。
再次检测布尔表达式。循环执行上面的过程。

下面介绍一个代码案例:

public static void main(String[] args) {
        for (int code = 0; code < 12; code++) {
            System.out.println("code=" + code);
        }
}

在Java中还有增强型for循环,Java5 引入了一种主要用于数组的增强型 for 循环。
Java 增强 for 循环语法格式如下:

for(声明语句 : 表达式)
{
   //代码句子
}

声明语句:声明新的局部变量,该变量的类型必须和数组元素的类型匹配。其作用域限定在循环语句块,其值与此时数组元素的值相等。

表达式:表达式是要访问的数组名,或者是返回值为数组的方法。

相比而言,这一增强型for循环使用的频率较高,常用来循环一个List,代码结构更加清晰。下面介绍一个代码案例:

public static void main(String[] args) {
        List<Student> list = new ArrayList<>();
        for (int code = 1; code < 10; code++) {
            Student student = new Student();
            student.setId(code);
            list.add(student);
        }
        for (Student student : list) {
            System.out.println(student.getId());
        }
}

Java面向对象

在这里我们暂时不探究什么是面向对象,我们用实际的案例先来感受下面向过程和面向对象之间的一个直观区别和感受: -五子棋案例

面向过程模式下的五子棋下棋的案例模式:

/**
 * 五子棋讲述面向对象
 *
 * @param args args
 */
public static void main(String[] args) {
    // 开始游戏
    start();
    //设定一个阈值判别当前下棋的用户,默认黑子先行
    boolean blackChess = false;
    //判断是否已经有输赢,有则结束,无则继续下棋
    while (judge()) {
        //黑方先行
        if (!blackChess) {
            //黑子落子
            blackChess();
            //将黑子落子置为 true
            blackChess = true;
            //绘制棋盘
            mapping();
        } else {
            //白子落子
            whiteChess();
            //将黑子落子置为false
            blackChess = false;
            //绘制棋盘
            mapping();
        }
    }
    //输出最终结果
    outputResults();
}

/**
 * 开始游戏
 */
static void start() {
    //some codes
}

/**
 * 黑子下
 */
static void blackChess() {
    //some codes
}

/**
 * 白子下
 */
static void whiteChess() {
    //some codes
}

/**
 * 判断输赢
 */
static boolean judge() {
    //some codes
    return false;
}

/**
 * 绘制画面
 */
static void mapping() {
    //some codes
}

/**
 * 输出结果
 */
static void outputResults() {
    //some codes
}

面向对象模式下的五子棋

/**
 * 棋盘系统
 */
public static class Board {

    /**
     * 棋盘背景
     */
    private String boardBackground;

    public Board(String boardBackground) {
        this.boardBackground = boardBackground;
    }

    /**
     * 绘制画面
     */
    public void mapping() {
        //some codes
    }

    public boolean judge() {
        //some codes
        return false;
    }

    public User forerunner(User userA, User userB) {
        //根据一些决策模式,随机挑选出一个执行的用户
        return userA;
    }

    /**
     * 输出结果
     */
    static void outputResults() {
        //some codes
    }

    /**
     * 决策系统
     *
     * @param userA 用户1
     * @param userB 用户2
     */
    public void race(User userA, User userB) {
        User forerunner = forerunner(userA, userB);
        while (judge()) {
            forerunner.chess();
            mapping();
            if (forerunner.color.equals(userA.color)) {
                forerunner = userA;
            } else {
                forerunner = userB;
            }
        }
        outputResults();
    }
}

/**
 * 下棋的用户
 */
public static class User {

    /**
     * 下棋方颜色  黑/白
     */
    private String color;

    public User(String color) {
        this.color = color;
    }

    /**
     * 下棋
     */
    public void chess() {
    }
}

public static void main(String[] args) {
    Board board = new Board("极其好看的棋盘,炫到离谱");
    //调用棋盘系统,渲染棋盘信息
    board.mapping();
    User userA = new User("我是紫色的");
    User userB = new User("我是红橙黄绿蓝靛紫色的");
    board.race(userA, userB);
}

在当前数量下的代码,看起来面向过程比面向对象更加的简洁,但是两段代码比较来讲,面向过程的程序更加的耦合,规则定义更加的硬板一些(从谁先行,黑白棋颜色来看----不考虑五子棋的常规规则),而面向对象模式下的五子棋可以简单化的拥有更多的扩展(当前还未展示继承、多态等特性),这段代码只是感受一下面向对象和面向过程的区别,案例可能不是那么明显,下面我们再来看一个例子:

我们拿 蛋炒饭 和 盖浇饭 来举例阐述下面向对象和面向过程:
在这里插入图片描述
在这里插入图片描述
上面这图就是蛋炒饭和盖浇饭,从图里面已经能够分析出蛋炒饭最直观的感受就是耦合度,蛋炒饭的耦合度明显大于盖浇饭:

蛋炒饭制作的细节,我不太清楚,因为我没当过厨师,也不会做饭,但最后的一道工序肯定是把米饭和鸡蛋混在一起炒匀。

盖浇饭呢,则是把米饭和盖菜分别做好,你如果要一份红烧肉盖饭呢,就给你浇一份红烧肉;如果要一份青椒土豆盖浇饭,就给浇一份青椒土豆丝。蛋炒饭的好处就是入味均匀,吃起来香。如果恰巧你不爱吃鸡蛋,只爱吃青菜的话,那么唯一的办法就是全部倒掉,重新做一份青菜炒饭了。盖浇饭就没这么多麻烦,你只需要把上面的盖菜拨掉,更换一份盖菜就可以了。盖浇饭的缺点是入味不均,可能没有蛋炒饭那么香。

如果要评判出面向对象(盖浇饭)和面向过程(蛋炒饭)孰优孰劣,必须要在某一个具体的场景下才能说明,不是说面向对象一定比面向过程更好,只是面向对象在某些条件下,确实比面向过程更有应用性。

好了,言归正传,我们正式开始来讲述Java面向对象:

Java类

要想学废Java面向对象,那我们首先来认识一下Java类(class),简单认识下成员变量和成员方法:

package cn.smart.transfer.server.java;

public class Student {

    private String name;

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Student() {

    }

    public Student(String name) {
        this.name = name;
    }

    public void printName() {
        System.out.println(this.name);
    }
}

说明:在上面的Java程序中,我们定义了一个Java类Student,指定了访问修饰符public,给类Student定义了成员变量(属性)name,定义了两个构造函数(后面会说明),其中一个是没有参数的,另一个是指定了一个String类型的参数的,提供了类成员变量的访问方法getName,提供了类变量的写入方法setName,提供了成员方法printName;

我们先来看看类定义的说明:

Java类是由关键字class修饰的数据类型,是组成Java程序的基本要素,所有的Java程序都是基于类的

我们先摒弃之前写的所有的代码,从零开始来学习Java类:

class ClassName{
}

Java类的定义格式如上,Java类名称定义时我们有如下约定:

1.类名应该以下划线()或字母开头,最好以字母开头
2.第一个字母最好大写,如果类名由多个单词组成,则每个单词的首字母最好都大写
3.类名不能为 Java 中的关键字,例如 boolean、this、int 等
4.类名不能包含任何嵌入的空格或点号以及除了下划线(
)和美元符号($)字符之外的特殊字符

在实际开发过程中,我们对类名称要求更加严格,最常见的便是类名是驼峰式的由字母组成的,不包含其它字符,但这些都不是强制要求,只是一种约定俗成的规范。

一个Java文件(后缀 .java)可以定义多个类吗?,这个是可以的,但是我们需要遵循一个规则:

Java 允许在一个Java源文件中编写多个类,但至多只能有一个类使用public修饰
在这里插入图片描述
上图这种情况,就是需要避免的问题,我们可以在一个Java文件中定义多个类,但是只能有一个由访问修饰符public修饰的类,否则编译器会报错。

内部类

当然,如果定义的是内部类,那么这个规则可以无视。我们可以看下内部类的定义:

在类内部可定义成员变量和方法,且在类内部也可以定义另一个类。如果在类 Outer 的内部再定义一个类 Inner,此时类 Inner 就称为内部类(或称为嵌套类),而类 Outer 则称为外部类(或称为宿主类)。

我们先来看个内部类的例子:

public class Test {

    public class InnerClass {
        public int getSum(int x,int y) {
            return x + y;
        }
    }
    
    public static void main(String[] args) {
        Test.InnerClass ti = new Test().new InnerClass();
        int i = ti.getSum(2,3);
        // 输出5
        System.out.println(i);
    }
}

内部类的特点如下:
内部类仍然是一个独立的类,在编译之后内部类会被编译成独立的.class文件,但是前面冠以外部类的类名和$符号。
内部类不能用普通的方式访问。内部类是外部类的一个成员,因此内部类可以自由地访问外部类的成员变量,无论是否为 private 的。
内部类声明成静态的,就不能随便访问外部类的成员变量,仍然是只能访问外部类的静态成员变量。

内部类可以很好地实现隐藏,一般的非内部类是不允许有 private 与 protected 权限的,但内部类可以。内部类拥有外部类的所有元素的访问权限。

内部类可以分为:实例内部类、静态内部类和成员内部类,每种内部类都有它特定的一些特点,本节先详细介绍一些和内部类相关的知识。

在类 A 中定义类 B,那么类 B 就是内部类,也称为嵌套类,相对而言,类 A 就是外部类。如果有多层嵌套,例如类 A 中有内部类 B,而类 B 中还有内部类 C,那么通常将最外层的类称为顶层类(或者顶级类)。

访问修饰符

在上面的介绍信息中,不止一次提到了访问修饰符,那我们就来讲讲这个访问修饰符:

Java中的访问修饰符:是用来描述类,属性,方法的关键字。其作用是使被修饰的目标,指定在什么情况下可以被使用。

(1)public: 用public修饰的类、类属变量及方法,包内及包外的任何类(包括子类和普通类)均可以访问;

(2)protected: 用protected修饰的类、类属变量及方法,包内的任何类及包外那些继承了该类的子类才能访问(此处稍后解释),protected重点突出继承;

(3)default: 如果一个类、类属变量及方法没有用任何修饰符(即没有用public、protected及private中任何一种修饰),则其访问权限为default(默认访问权限)。默认访问权限的类、类属变量及方法,包内的任何类(包括继承了此类的子类)都可以访问它,而对于包外的任何类都不能访问它(包括包外继承了此类的子类)。default重点突出包;

(4)private: 用private修饰的类、类属变量及方法,只有本类可以访问,而包内包外的任何类均不能访问它。

访问级别访问修饰符同一个类同一个包子类不同的包
公开public
私有private×××
受保护protected×
默认无访问修饰符 default××

上面所讲述只是针对了Java类,那么我们在定义类方法时(对象行为),也会指定一些修饰符,我们称之为修饰方法的修饰符,这些说明如下:

public:被该修饰符修饰的方法可以被任何类通过对象.方法使用
protected:被该修饰符修饰的方法可以被该类自身、本包中的类、和子类(是子类而非父类)所使用
private:被该修饰符修饰的方法只能被该类使用
缺省:被该修饰符修饰的方法只能被该类、本包中的类所使用(缺省的意思就是不用写)
static:被static修饰的方法是一个静态方法,可以被类直接使用,可以通过类名.方法名直接调用,static可以和final一起使用但是不能和final一起使用
abstract:被abstract修饰的方法,不能写方法体,且该类必须是抽象类,抽象类中可以没有抽象方法,但是有抽象方法的类必须是一个抽象类
final:被该修饰符修饰的方法不能被重写’

还有对成员变量(对象属性)的修饰符,我们称之为成员变量修饰符:

public:被该修饰符修饰的成员变量可以被任何类使用
protected:被该修饰符修饰的成员变量能被该类自身、本包中的类、和子类(是子类而非父类)所使用,即用protected修饰的属性在其他包中的子类中可以通过子类对象进行访问,不能通过本类对象进行访问
private:被该修饰符修饰的成员变量只能被该类使用
缺省:被该修饰符修饰的成员变量只能被该类、本包中的类所使用
static:被static修饰的成员变量可以被类直接使用
final:被该修饰符修饰的成员变量是常量

针对该模块,我们对每句话都进行案例的分析来说明!!!!!!


上面我们介绍了Java类定义的一些基础知识,学会定义Java类,定义Java成员变量/方法,那接下来我们来具体说明下这个Java类应该怎么去理解:
实际上,面向对象确实是更贴近我们生活中所见的事务,更适合去描述我们见到的事物或事物间的联系,面向对象中将生活中具体的事物如狗、人、鸟、电脑等抽象成一个个具体的类,我们定义 class Dog ; class Person ; class Bird ; 实际上类的名称就是这些具体事物的名称。
我们讲狗有哪些属性(如毛发颜色等):

名字(比如:旺财)
年龄(活了几年这个属性总是有的)
品种(哈士奇啥啥啥的)
会不会拆家(会或者不会)

既然属性明确了,那我们把狗这个类就可以定义出来了呗:

public class Dog {

	/**
     * 狗狗名称
     */
    private String name;

    /**
     * 年龄
     */
    private int age;

    /**
     * 品种
     */
    private String type;

    /**
     * 会不会拆家
     */
    private boolean dismantleTheHouse;
}

这不,一个拥有4个属性的Java类不就定义出来了?别问我为什么用private,也别问我是不是可以用public,这个是访问限制的问题,不属于当前讨论范畴,更别问我这个String\int\boolean是什么东东,这个之前就说明过了。

有同学估计会说,这个狗虽然定义了这些类型,但是我们好像没办法定义一个狗狗出来,也没办法把这些属性的值给修改了,这里先说说怎么定义一个狗狗出来。在这之前先来讲讲构造函数!

构造函数

构造函数说白了就是让我们初始化一个具体的对象的,构造函数没有返回值(且不可定义返回值类型),他的函数名跟我们类名称相同,构造对象在类的建立过程中只运行一次。
先来说个例子,还是复用上面的狗狗的类:

public class Dog {

	/**
     * 狗狗名称
     */
    private String name;

    /**
     * 年龄
     */
    private int age;

    /**
     * 品种
     */
    private String type;

    /**
     * 会不会拆家
     */
    private boolean dismantleTheHouse;

	public Dog (){

    }

    public Dog (String name){
		this.name = name;
    }
}

先说下这个例子,构造函数可以使用private等修饰符修饰,没关系,但是如果你的这个构造函数是准备给其他类调用的,那我建议你定义成 public ,至于原因可以参照访问修饰符章节。
构造函数是可以重载的(方法的重载),当然,重载无非就是参数类型或参数数量上的区别了。
上面的例子里面,顶一个一个无参构造函数(敲重点,Java类当你没有明确定义构造函数时,会自动给你定义一个无参构造函数!BUT,当你定义了自己的构造函数之后,这个福利就没有了)
Java类当你没有明确定义构造函数时,会自动给你定义一个无参构造函数!
Java类当你没有明确定义构造函数时,会自动给你定义一个无参构造函数!
Java类当你没有明确定义构造函数时,会自动给你定义一个无参构造函数!
构造函数,见名知义,就是构造用的,就是通过传递一些参数快速构造出一个目标类,至于构造函数里面怎么去操作这些传递进来的参数这个就看具体代码逻辑了,并不是只能像上面代码那样,只能简单赋值

构造函数定义是说完了,问题来了,我该怎么调用构造函数呢!
这里引进一个关键字 new

new 关键字

先说说这个关键字是干哈的:

new 关键字的一个作用就是用来实例化对象的(就是我们要的具体拿到一个类对象的实例,这个过程称之为实例化),其用法如下:
ClassName className = new ClassName ();
注意了,new后面跟的就是你定义的构造函数!

new 关键字的另一个作用就是给数组分配元素的,这里举个栗子:
String[] array = new String[]{“1”,“2”,“3”};

更加复杂的内容,可以暂时不考虑,new关键字更像是一个分配内存的运算符。


我们说完了构造函数,也简单引进了关键字new,那接下来我们可以定义一个类了,怎么定义呢?

//指定目标类Dog 指定实例化出来的对象名称dog  = 就是赋值的,前面就是基础的运算符模式
//new 关键字  后面跟上的就是你定义的构造函数,有参数那就给参数
Dog dog = new Dog();
//下面的实例化调用的就是我们定义的一个String参数的构造函数
Dog dogB = new Dog("dogB");

这是实例化一个类的方法,当然,获取一个类的实例这并不是唯一的方法,Java中给我们提供了反射机制,也可以获取到对应的类实例。这点后期进阶内容里面会提到。

现在,我们已经可以拿到我们想要的旺财这个具体的对象实例了,并且通过构造方法我们还可以把旺财的名字给赋值进去,但是就单单一个名字是不是有点单调了,而且我要是还想赋值其它的字段呢,那么这时候我们可以通过两种方法进行,一种呢是修改我们属性的修饰符,从private变更为public,这样我们就可以直接调用或写入值了,另一种呢就是在类中提供一种可以修改这些属性的方法,我们这两种模式都聚举例子:

public class Dog {

	/**
     * 狗狗名称
     */
    public String name;

    /**
     * 年龄
     */
    public int age;

    /**
     * 品种
     */
    public String type;

    /**
     * 会不会拆家
     */
    public boolean dismantleTheHouse;

	public Dog (){

    }

    public Dog (String name){
		this.name = name;
    }
}

这种修改修饰符的方法不推荐!!,原因嘛就是破坏了我们的封装特性,让我们的对象属性都暴露在外,不安全。另一种提供特定方法的模式如下:

public class Dog {

	/**
     * 狗狗名称
     */
    private String name;

    /**
     * 年龄
     */
    private int age;

    /**
     * 品种
     */
    private String type;

    /**
     * 会不会拆家
     */
    private boolean dismantleTheHouse;

	public Dog (){

    }

    public Dog (String name){
		this.name = name;
    }

	public String getName(){
		return this.name;
	}
	
	public void setName(String name){
		this.name = name;
	}
}

通过创建了一个get/set方法实现对某一个属性的写入或读取内容,其本质是成员方法,只不过专用作访问我们可以暴露的属性方法了。

现在旺财有实例了,也可以修改类型颜色啥的了,但是总感觉还是少了啥,这个旺财不够生动啊,又不吃饭,又不喝水,又不吠叫,那怎么样才可以让旺财实现这些对象行为呢,这种可以通过我们的对象方法(成员方法)来实现,通过定义成员方法,然后使用特定的实例来进行调用,即可完成该对象的行为。我们用一个综合的例子来看一下:

public class Dog {

    /**
     * 狗狗名称
     */
    private String name;

    /**
     * 年龄
     */
    private int age;

    /**
     * 品种
     */
    private String type;

    /**
     * 会不会拆家
     */
    private boolean dismantleTheHouse;

    public Dog(String name, int age, String type) {
        this.name = name;
        this.age = age;
        this.type = type;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public boolean isDismantleTheHouse() {
        return dismantleTheHouse;
    }

    public void setDismantleTheHouse(boolean dismantleTheHouse) {
        this.dismantleTheHouse = dismantleTheHouse;
    }

    public void drinkWater() {
        System.out.println("狗狗 - " + name + " 喝水了");
    }

    public void eat() {
        System.out.println("狗狗 - " + name + " 吃了一个大鸡腿");
    }

    public void bark() {
        System.out.println("狗狗 - " + name + " 叫个不停");
    }

    public static void main(String[] args) {
        Dog dog = new Dog("旺财", 3, "阿拉斯加");
        dog.setDismantleTheHouse(true);
        dog.bark();
        dog.eat();
        dog.drinkWater();
        if (dog.isDismantleTheHouse()) {
            System.out.println("狗狗:" + dog.getName() + "会拆家");
        } else {
            System.out.println("狗狗:" + dog.getName() + "很安静,不会拆家");
        }
    }
}

就此,Java对象的基础介绍就到这里,接下来我们再具体看看详细的Java面向对象的相关特性。

面向对象特性–封装性

封装作为

Java基础进阶内容

队列

队列是一种由数组和链表作为底层构造的只暴露头和尾操作API的数据结构,因此,队列是被认为是一种受限的数据结构。
队列的特性是:先进先出,类似于排队
队列-先进先出示意
Java中的Queue继承自Collection,同Set与List
在这里插入图片描述

Queue

常用方法基本说明和列举

/**
 * 向队列压入元素,当超出容量抛出异常,正常操作返回 true
 */
boolean add(E e);

/**
 * 向队列压入元素,当超出容量返回false,正常操作返回 true
 */
boolean offer(E e);
/**
 * 检索并删除此队列的头。此方法与poll()的不同之处在于,如果此队列为空,它将抛出异常。
 * 返回:这个队列的头
 * 抛出:NoTouchElementException–如果此队列为空
 */
E remove();

/**
 * 检索并删除此队列的头,如果此队列为空,则返回null。
 * 返回:此队列的头,如果此队列为空,则为空
 */
E poll();
/**
 * 检索但不删除此队列的头。此方法与peek的不同之处在于,如果队列为空,它将抛出异常。
 * 返回:此队列的头
 * 抛出:NoTouchElementException–如果此队列为空
 */
E element();

/**
 * 检索但不删除此队列的头,如果此队列为空,则返回null。
 * 返回:此队列的头,如果此队列为空,则返回null
 */
E peek();

双端队列

Deque 是 “double ended queue” 的缩写,双端队列两个端口都可以进出
Deque子类
Deque的实现类是LinkedList,ArrayDeque,LinkedBlockingDeque,其中LinkedList是最常用的。值得注意的是,LinkedList也实现了List接口。
LinkedList也继承了List
双端队列常用方法列举

/**
 * 如果可以在不违反容量限制的情况下立即插入指定元素,则在该deque的前面插入指定元素。
 * 如果当前没有可用空间,则抛出IllegalStateException。
 * 当使用容量受限的deque时,通常最好使用方法OFFEFIRST。
 * 
 * 参数:e–要添加的元素
 * 抛出:
 * 		IllegalStateException–如果由于容量限制,此时无法添加元素
 * 		ClassCastException–如果指定元素的类阻止将其添加到此deque中
 *		NullPointerException–如果指定的元素为空,并且此deque不允许空元素
 *  	IllegalArgumentException–如果指定元素的某些属性阻止将其添加到此deque
 */
void addFirst(E e);

/**
 * 如果可以在不违反容量限制的情况下立即插入指定元素,则在该deque的末尾插入指定元素。
 * 如果当前没有可用空间,则抛出IllegalStateException。
 * 此方法相当于add。
 * 
 * 参数:e–要添加的元素
 * 抛出:
 * 		IllegalStateException–如果由于容量限制,此时无法添加元素
 * 		ClassCastException–如果指定元素的类阻止将其添加到此deque中
 * 		NullPointerException–如果指定的元素为空,并且此deque不允许空元素
 * 		IllegalArgumentException–如果指定元素的某些属性阻止将其添加到此deque
 */
void addLast(E e);

/**
 * 在该deque的末尾插入指定元素,除非它违反容量限制。
 * 当使用容量受限的deque时,此方法通常优于addLast方法,后者只能通过抛出异常来插入元素。
 * 
 * 参数:e–要添加的元素
 * 返回:如果元素被添加到此deque,则为true,否则为false
 * 抛出:
 * 		ClassCastException–如果指定元素的类阻止将其添加到此deque中
 * 		NullPointerException–如果指定的元素为空,并且此deque不允许空元素
 * 		IllegalArgumentException–如果指定元素的某些属性阻止将其添加到此deque
 */
boolean offerFirst(E e);

/**
 * 在该deque的末尾插入指定元素,除非它违反容量限制。
 * 当使用容量受限的deque时,此方法通常优于addLast方法,后者只能通过抛出异常来插入元素。
 * 
 * 参数:e–要添加的元素
 * 返回:如果元素被添加到此deque,则为true,否则为false
 * 抛出:
 * 		ClassCastException–如果指定元素的类阻止将其添加到此deque中
 * 		NullPointerException–如果指定的元素为空,并且此deque不允许空元素
 * 		IllegalArgumentException–如果指定元素的某些属性阻止将其添加到此deque
 */
boolean offerLast(E e);

/**
 * 检索并删除此deque的第一个元素。此方法与pollFirst的不同之处在于,如果此deque为空,它将抛出异常。
 * 返回:这个德克的头
 * 抛出:NoTouchElementException–如果此deque为空
 */
E removeFirst();

/**
 * 检索并删除此deque的最后一个元素。此方法与pollLast的不同之处在于,如果此deque为空,它将抛出异常。
 * 返回:这个deque的尾巴
 * 抛出:NoTouchElementException–如果此deque为空
 */
E removeLast();

/**
 * 检索并删除此deque的第一个元素,如果此deque为空,则返回null。
 * 返回:此deque的头,如果此deque为空,则为空
 */
E pollFirst();

/**
 * 检索并删除此deque的最后一个元素,如果此deque为空,则返回null。
 * 返回:此deque的尾部,如果此deque为空则为空
 */
E pollLast();

/**
 * 检索但不删除此deque的第一个元素。此方法与peekFirst的不同之处在于,如果此deque为空,它将抛出异常。
 * 返回:这个deque的头
 * 抛出:NoTouchElementException–如果此deque为空
 */
E getFirst();

/**
 * 检索但不删除此deque的最后一个元素。此方法与peekLast的不同之处在于,如果此deque为空,它将抛出异常。
 * 返回:这个deque的尾巴
 * 抛出:NoTouchElementException–如果此deque为空
 */
E getLast();

/**
 * 检索但不删除此deque的第一个元素,如果此deque为空,则返回null。
 * 返回:此deque的头,如果此deque为空,则为空
 */
E peekFirst();

/**
 * 检索但不删除此deque的最后一个元素,如果此deque为空,则返回null。
 * 返回:此deque的尾部,如果此deque为空则为空
 */
E peekLast();

双端队列既能实现先进先出,又能实现先进后出,所以,双端队列可以有以下三种用途:

//1.作为普通队列(先进先出)
Queue queue = new LinkedList();
Deque deque = new LinkedList();
//2.作为堆栈(先进后出),java中堆栈Stack类已经过时,官方推荐使用Deque代替Stack使用
Deque deque = new LinkedList();
//3.作为双端队列(两端均可进出)
Deque deque = new LinkedList();

上面提到的使用Deque替代Stack,其中涉及到的等效方法如下:

堆栈方法Deque方法方法说明
push(E e)addFirst(E e)元素入栈
pop()removeFirst()元素出栈
peek()peekFirst()获取头元素,该元素不删除

阻塞队列

阻塞队列是一个支持两个附加操作的队列,队列存储达到上限时,存储元素会等待队列可用;当队列为空时,获取元素的线程会等待队列为非空。
Java中阻塞队列有 BlockingQueue 以及 BlockingDeque,见名可知后者为双向队列。这里不再列举对应的方法,而是使用案例来进行介绍说明:
BlockingQueue

方法描述抛出异常返回特殊值一直阻塞超时退出
插入数据add(e)offer(e)put(e)offer(e,time,unit)
获取并移除队列的头remove()poll()take()poll(time,unit)
获取但不移除队列的头element()peek()不可用不可用

抛出异常

是指当阻塞队列满时候,再往队列里插入元素,会抛出IllegalStateException(“Queue full”)异常。当队列为空时,从队列里获取元素时会抛出NoSuchElementException异常 。

返回特殊值

插入方法会返回是否成功,成功则返回true。移除方法,则是从队列里拿出一个元素,如果没有则返回null。

一直阻塞

当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到拿到数据,或者响应中断退出。当队列空时,消费者线程试图从队列里take元素,队列也会阻塞消费者线程,直到队列可用。

超时退出

当阻塞队列满时,队列会阻塞生产者线程一段时间,如果超过一定的时间,生产者线程就会退出。

BlockingQueue是一个接口,它的实现类有ArrayBlockingQueue、DelayQueue、 LinkedBlockingDeque、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue等,它们的区别主要体现在存储结构上或对元素操作上的不同.

add方法添加数据

public class QueueTest {

    private final static int QUEUE_SIZE = 100;

    private static final BlockingQueue<String> BLOCKING_QUEUE = new ArrayBlockingQueue<String>(QUEUE_SIZE);

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            //add方法调用时,如果超出限制数量将报错
            BLOCKING_QUEUE.add("" + i);
        }
    }
}

add方法操作满队列的队列
offer方法添加数据

public class QueueTest {

    private final static int QUEUE_SIZE = 2;

    private static final BlockingQueue<String> BLOCKING_QUEUE = new ArrayBlockingQueue<String>(QUEUE_SIZE);

    public static void main(String[] args) {
        for (int i = 0; i < 4; i++) {
            //offer方法调用时,数据插入正常返回true,否则返回false
            System.out.println(BLOCKING_QUEUE.offer("" + i));
        }
    }
}

offer方法操作满队列队列
put方法调用

public class QueueTest {

    private final static int QUEUE_SIZE = 2;

    private static final BlockingQueue<String> BLOCKING_QUEUE = new ArrayBlockingQueue<>(QUEUE_SIZE);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 4; i++) {
            System.out.println("当前i="+i+",看到我停止了那么队列就被阻塞了,如果没有其他操作,那么我会一直阻塞");
            BLOCKING_QUEUE.put("i=" + i);
        }
    }
}

offer定时方法调用

public class QueueTest {

    private final static int QUEUE_SIZE = 2;

    private static final BlockingQueue<String> BLOCKING_QUEUE = new ArrayBlockingQueue<>(QUEUE_SIZE);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            System.out.println("当前i=" + i + ",看到我停止了那么队列就被阻塞了,当时间超出,那么我会停止阻塞,但是数据也不会被放进队列");
            //offer方法调用时,数据插入正常返回true,否则返回false
            boolean offer = BLOCKING_QUEUE.offer("i=" + i, 3, TimeUnit.SECONDS);
            if (offer) {
                System.out.println("数据被正常放进队列,不信你看:" + BLOCKING_QUEUE);
            } else {
                System.out.println("数据没有被放进队列,我虽然不阻塞了,但是这条数据还是弄丢了,不信你瞅瞅:" + BLOCKING_QUEUE);
            }
        }
    }
}

BlockingDeque

如果线程同时生成和使用同一队列的元素,则可以使用BlockingDeque。如果生成线程需要在队列的两端插入元素,并且消费线程需要从队列的两端移除元素,那么也可以使用它:
线程将生成元素并将它们插入队列的任一端。如果deque当前已满,则插入线程将被阻塞,直到删除线程将元素从双端队列中取出。如果deque当前为空,则将阻止删除线程,直到插入线程将元素插入到双端队列中。

  • 头部元素
方法描述抛出异常返回特殊值一直阻塞超时退出
插入数据addFirst(e)offerFirst(e)putFirst(e)offerFirst(e,time,unit)
获取并移除队列的头removeLast()pollFirst()takeFirst()pollFirst(time,unit)
获取但不移除队列的头getFirst()peekFirst()不可用不可用
  • 尾部元素
方法描述抛出异常返回特殊值一直阻塞超时退出
插入数据addLast(e)offerLast(e)putLast(e)offerLast(e,time,unit)
获取并移除队列的头removeLast()pollLast()takeLast()pollLast(time,unit)
获取但不移除队列的头getLast()peekLast()不可用不可用

非阻塞队列

案例讲解-延时队列的应用

案例说明:以发送短信为案例,要求短信发送需要设定一个时间来进行推送,如推广短信设置某一时间节点进行推送。

首先分析该问题,我们系统进行短信发送,可以分为两种模式,一种是用户触发,比如手机验证码等,这种短信都是及时发送出去,即触发后立即发送;还有一种模式,就是我在系统中定时一个任务(这里不考虑定时任务触发,而是考虑短信立即生成后延时推送),将本该立即推送的短信延迟一段时间后再发送,比如版本更新(可能凌晨才推送)等,那么这时候就可以使用延时队列来完成该功能(真实项目一般会使用消息队列中间件)。

我们这里简单的模拟短信发送流程:

@Data
public class Sms implements Delayed {

    /**
     * 消息信息
     */
    private String message;

    /**
     * 推送号码
     */
    private String phone;

    /**
     * 发送时间 - 定时
     */
    private Date sendTime;

    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(this.sendTime.getTime() - System.currentTimeMillis(), TimeUnit.MICROSECONDS);
    }

    @Override
    public int compareTo(Delayed delayed) {
        Sms delayQueue = (Sms) delayed;
        return this.sendTime.before(delayQueue.sendTime) ? 0 : 1;
    }
}

@Slf4j
public class SmsDelayedTest {

    /**
     * sms延时队列
     */
    private static final DelayQueue<Sms> SMS_DELAY_QUEUE = new DelayQueue<>();

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 10; i++) {
            Sms sms = new Sms();
            sms.setMessage("这是一条测试的message" + i);
            sms.setPhone(i + "");
            sms.setSendTime(SystemDateUtils.getRuleTime(new Date(), i*10,Calendar.SECOND,false));
            SMS_DELAY_QUEUE.offer(sms);
        }

        while (true) {
            Sms sms = SMS_DELAY_QUEUE.take();
            log.info("【短信发送】短信发送时间:{} ,短信设定的发送时间:{},基本信息:{}", SystemDateUtils.formatDate(new Date()), SystemDateUtils.formatDate(sms.getSendTime()), JSONObject.toJSONString(sms));
        }
    }
}

从案例中我们使用了DelayQueue,这里介绍下DelayQueue:
DelayQueue继承关系
从继承关系里面可以看出DelayQueue是阻塞队列BlockingQueue的一种实现,DelayQueue的泛型参数需要实现Delayed接口,Delayed接口继承了Comparable接口,DelayQueue内部使用非线程安全的优先队列(PriorityQueue),并使用Leader/Followers模式,最小化不必要的等待时间。DelayQueue不允许包含null元素。

DelayQueue是有序的队列,越接近头部的元素更接近当前时间,理论上,只要内存足够,该队列中可以放置的元素无限制。

本文配有相关的视频讲述,有需要者可联系博主。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dp_shiyu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值