JAVA-常用类和基础API

第11章_常用类和基础API

本章专题与脉络

第3阶段:Java高级应用-第11章

1. 字符串相关类之不可变字符序列:String

1.1 String的特性

  • java.lang.String 类代表字符串。Java程序中所有的字符串文字(例如"hello" )都可以看作是实现此类的实例。

  • 字符串是常量,用双引号引起来表示。它们的值在创建之后不能更改。

  • 字符串String类型本身是final声明的,意味着我们不能继承String。

  • String对象的字符内容是存储在一个字符数组value[]中的。"abc" 等效于 char[] data={'h','e','l','l','o'}

    image-20220514184404024
    1
    2
    3
    4
    5
    6
    7
    //jdk8中的String源码:
    public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[]; //String对象的字符内容是存储在此数组中
     
    /** Cache the hash code for the string */
    private int hash; // Default to 0
    • private意味着外面无法直接获取字符数组,而且String没有提供value的get和set方法。

    • final意味着字符数组的引用不可改变,而且String也没有提供方法来修改value数组某个元素值

    • 因此字符串的字符数组内容也不可变的,即String代表着不可变的字符序列。即,一旦对字符串进行修改,就会产生新对象。

    • JDK9只有,底层使用byte[]数组。

      1
      2
      3
      4
      5
      6
      7
      8
      public final class String implements java.io.Serializable, Comparable<String>, CharSequence { 
      @Stable
      private final byte[] value;
      }

      //官方说明:... that most String objects contain only Latin-1 characters. Such characters require only one byte of storage, hence half of the space in the internal char arrays of such String objects is going unused.

      //细节:... The new String class will store characters encoded either as ISO-8859-1/Latin-1 (one byte per character), or as UTF-16 (two bytes per character), based upon the contents of the string. The encoding flag will indicate which encoding is used.
  • Java 语言提供对字符串串联符号(”+”)以及将其他对象转换为字符串的特殊支持(toString()方法)。

1.2 String的内存结构

1.2.1 概述

因为字符串对象设计为不可变,所以字符串有常量池来保存很多常量对象。

JDK6中,字符串常量池在方法区。

JDK7开始,就移到堆空间。

举例内存结构分配:

image-20220405160036240

1.2.2 练习类型1:拼接

1
2
3
4
5
String s1 = "hello";
String s2 = "hello"; //字符串常量池不允许存放两个相同的字符串常量
//因此s1和s2其实指向是统一位置
System.out.println(s1 == s2);
// 内存中只有一个"hello"对象被创建,同时被s1和s2共享。

对应内存结构为:(以下内存结构以JDK6为例绘制):

image-20220405152839525

进一步:

image-20220405152941599
1
2
3
4
5
6
7
8
9
10
11
//新建一个对象,name是自己特有的
Person p1 = new Person();
p1.name = “Tom";
//新建一个对象 name是自己特有的
Person p2 = new Person();
p2.name = “Tom";
System.out.println(p1.name.equals( p2.name)); //true
System.out.println(p1.name == p2.name); //true
System.out.println(p1.name == "Tom"); //true
p1.name="Jerry";
System.out.println(p2.name); //Tom 因为改变p1不影响p2
image-20220405153027693

1.2.3 练习类型2:new

String str1 = “abc”; 与 String str2 = new String(“abc”);的区别?

image-20220405160149200

str2 首先指向堆中的一个字符串对象,然后堆中字符串的value数组指向常量池中常量对象的value数组。

  • 字符串常量存储在字符串常量池,目的是共享。

  • 字符串非常量对象存储在堆中。

练习:

1
2
3
4
5
6
7
8
9
String s1 = "javaEE";
String s2 = "javaEE";
String s3 = new String("javaEE");
String s4 = new String("javaEE");

System.out.println(s1 == s2);//true s1和s2直接指向同一位置
System.out.println(s1 == s3);//false s3相当于创建对象之后再指向
System.out.println(s1 == s4);//false s4相当于创建对象之后再指向
System.out.println(s3 == s4);//false s3和s4只想内容一样 但是堆地址不同
image-20220405160321172

练习:String str2 = new String(“hello”); 在内存中创建了几个对象?

1
两个

1.2.4 练习类型3:intern()

  • String s1 = “a”;

说明:在字符串常量池中创建了一个字面量为”a”的字符串。

  • s1 = s1 + “b”;

说明:实际上原来的“a”字符串对象已经丢弃了,现在在堆空间中产生了一个字符串s1+”b”(也就是”ab”)。如果多次执行这些改变串内容的操作,会导致大量副本字符串对象存留在内存中,降低效率。如果这样的操作放到循环中,会极大影响程序的性能。

  • String s2 = “ab”;

说明:直接在字符串常量池中创建一个字面量为”ab”的字符串。

  • String s3 = “a” + “b”;

说明:s3指向字符串常量池中已经创建的”ab”的字符串。

  • String s4 = s1.intern();

说明:堆空间的s1对象在调用intern()之后,会将常量池中已经存在的”ab”字符串赋值给s4。

练习:

1
2
3
4
5
6
7
8
9
10
11
String s1 = "hello";
String s2 = "world";
String s3 = "hello" + "world";
String s4 = s1 + "world";
String s5 = s1 + s2;
String s6 = (s1 + s2).intern();

System.out.println(s3 == s4); //false
System.out.println(s3 == s5); //false
System.out.println(s4 == s5); //false
System.out.println(s3 == s6); //true

结论:

(1)常量+常量:结果是常量池。且常量池中不会存在相同内容的常量。

(2)常量与变量 或 变量与变量:结果在堆中

(3)拼接后调用intern方法:返回值在常量池中

练习:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Test
public void test01(){
String s1 = "hello";
String s2 = "world";
String s3 = "helloworld";

String s4 = s1 + "world";//s4字符串内容也helloworld,s1是变量,"world"常量,变量 + 常量的结果在堆中
String s5 = s1 + s2;//s5字符串内容也helloworld,s1和s2都是变量,变量 + 变量的结果在堆中
String s6 = "hello" + "world";//常量+ 常量 结果在常量池中,因为编译期间就可以确定结果
System.out.println(s3 == s6);//true
System.out.println(s3 == s4);//false
System.out.println(s3 == s5);//false
System.out.println(s3 == s6);//true
}

@Test
public void test02(){
final String s1 = "hello"; //加了final 就成为常量+常量情况
final String s2 = "world"; //加了final 就成为常量+常量情况
String s3 = "helloworld";

String s4 = s1 + "world";//s4字符串内容也helloworld,s1是常量,"world"常量,常量+常量结果在常量池中
String s5 = s1 + s2;//s5字符串内容也helloworld,s1和s2都是常量,常量+ 常量 结果在常量池中
String s6 = "hello" + "world";//常量+ 常量 结果在常量池中,因为编译期间就可以确定结果

System.out.println(s3 == s4);//true
System.out.println(s3 == s5);//true
System.out.println(s3 == s6);//true
}

@Test
public void test01(){
String s1 = "hello";
String s2 = "world";
String s3 = "helloworld";

String s4 = (s1 + "world").intern(); //把拼接的结果放到常量池中
String s5 = (s1 + s2).intern(); //把拼接的结果放到常量池中

System.out.println(s3 == s4);//true
System.out.println(s3 == s5);//true
}

练习:下列程序运行的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestString {
public static void main(String[] args) {
String str = "hello";
String str2 = "world";
String str3 ="helloworld";

String str4 = "hello".concat("world");
String str5 = "hello"+"world";

System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
}
}

concat方法拼接,哪怕是两个常量对象拼接,结果也是在堆。

练习:下列程序运行的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class StringTest {

String str = new String("good");
char[] ch = { 't', 'e', 's', 't' };

public void change(String str, char ch[]) {
str = "test ok";
ch[0] = 'b';
}
public static void main(String[] args) {
StringTest ex = new StringTest();
ex.change(ex.str, ex.ch);
System.out.print(ex.str + " and ");//
System.out.println(ex.ch);
}
}

1.3 String的常用API-1

1.3.1 构造器

  • public String() :初始化新创建的 String对象,以使其表示空字符序列。
  • publc String(String original): 初始化一个新创建的 String 对象,使其表示一个与参数相同的字符序列;换句话说,新创建的字符串是该参数字符串的副本。
  • public String(char[] value) :通过当前参数中的字符数组来构造新的String。
  • public String(char[] value,int offset, int count) :通过字符数组的一部分来构造新的String。
  • public String(byte[] bytes) :通过使用平台的默认字符集解码当前参数中的字节数组来构造新的String。
  • public String(byte[] bytes,String charsetName) :通过使用指定的字符集解码当前参数中的字节数组来构造新的String。

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//字面量定义方式:字符串常量对象
String str = "hello";

//构造器定义方式:无参构造
String str1 = new String();

//构造器定义方式:创建"hello"字符串常量的副本
String str2 = new String("hello");

//构造器定义方式:通过字符数组构造
char chars[] = {'a', 'b', 'c','d','e'};
String str3 = new String(chars);
String str4 = new String(chars,0,3);

//构造器定义方式:通过字节数组构造
byte bytes[] = {97, 98, 99 };
String str5 = new String(bytes);
String str6 = new String(bytes,"GBK"); //指定默认字符集类型
1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
char[] data = {'h','e','l','l','o','j','a','v','a'};
String s1 = String.copyValueOf(data);
String s2 = String.copyValueOf(data,0,5);
int num = 123456;
String s3 = String.valueOf(num);

System.out.println(s1);
System.out.println(s2);
System.out.println(s3);
}

1.3.2 String与其他结构间的转换

字符串 –> 基本数据类型、包装类:

  • Integer包装类的public static int parseInt(String s):可以将由“数字”字符组成的字符串转换为整型。
  • 类似地,使用java.lang包中的Byte、Short、Long、Float、Double类调相应的类方法可以将由“数字”字符组成的字符串,转化为相应的基本数据类型。

基本数据类型、包装类 –> 字符串:

  • 调用String类的public String valueOf(int n)可将int型转换为字符串

  • 相应的valueOf(byte b)、valueOf(long l)、valueOf(float f)、valueOf(double d)、valueOf(boolean b)可由参数的相应类型到字符串的转换。

    字符数组 –> 字符串:

  • String 类的构造器:String(char[]) 和 String(char[],int offset,int length) 分别用字符数组中的全部字符和部分字符创建字符串对象。

    字符串 –> 字符数组:

  • public char[] toCharArray():将字符串中的全部字符存放在一个字符数组中的方法。

  • public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin):提供了将指定索引范围内的字符串存放到数组中的方法。

字符串 –> 字节数组:(编码)

  • public byte[] getBytes() :使用平台的默认字符集将此 String 编码为 byte 序列,并将结果存储到一个新的 byte 数组中。

  • public byte[] getBytes(String charsetName) :使用指定的字符集将此 String 编码到 byte 序列,并将结果存储到新的 byte 数组。

    字节数组 –> 字符串:(解码)

  • String(byte[]):通过使用平台的默认字符集解码指定的 byte 数组,构造一个新的 String。

  • String(byte[],int offset,int length) :用指定的字节数组的一部分,即从数组起始位置offset开始取length个字节构造一个字符串对象。

  • String(byte[], String charsetName ) 或 new String(byte[], int, int,String charsetName ):解码,按照指定的编码方式进行解码。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void test01() throws Exception {
String str = "中国";
System.out.println(str.getBytes("ISO8859-1").length);// 2
// ISO8859-1把所有的字符都当做一个byte处理,处理不了多个字节
System.out.println(str.getBytes("GBK").length);// 4 每一个中文都是对应2个字节
System.out.println(str.getBytes("UTF-8").length);// 6 常规的中文都是3个字节

/*
* 不乱码:(1)保证编码与解码的字符集名称一样(2)不缺字节
*/
System.out.println(new String(str.getBytes("ISO8859-1"), "ISO8859-1"));// 乱码
System.out.println(new String(str.getBytes("GBK"), "GBK"));// 中国
System.out.println(new String(str.getBytes("UTF-8"), "UTF-8"));// 中国
}

1.4 String的常用API-2

String 类包括的方法可用于检查序列的单个字符、比较字符串、搜索字符串、提取子字符串、创建字符串副本并将所有字符全部转换为大写或小写。

1.4.1 系列1:常用方法

(1)boolean isEmpty():字符串是否为空
(2)int length():返回字符串的长度
(3)String concat(xx):拼接
(4)boolean equals(Object obj):比较字符串是否相等,区分大小写
(5)boolean equalsIgnoreCase(Object obj):比较字符串是否相等,不区分大小写
(6)int compareTo(String other):比较字符串大小,区分大小写,按照Unicode编码值比较大小
(7)int compareToIgnoreCase(String other):比较字符串大小,不区分大小写
(8)String toLowerCase():将字符串中大写字母转为小写
(9)String toUpperCase():将字符串中小写字母转为大写
(10)String trim():去掉字符串前后空白符
(11)public String intern():结果在常量池中共享

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class StringMethod {
public static void main(String[] args) {
String str="abcdef";
String str1="ABCDEF";
String str11="abCdef";
String str111=" afs asd ";
//1.判断字符串是否为空 --isEmpty()
System.out.println(str.isEmpty()); //false
//2.返回字符串长度 --length()
System.out.println(str.length()); //6
//3.拼接两个字符串 返回拼接的字符串 --concat(XX)
String str2="中国";
System.out.println(str.concat(str2)); //abcdef中国

//4.比较字符串是否相等【区分大小写】
System.out.println(str.equals(str1)); //false
//5.比较字符串是否相等【不区分大小写】
System.out.println(str.equalsIgnoreCase(str1)); //true 不区分大小写就是一个字符串

//6.比较字符串大小【区分大小写】
System.out.println(str.compareTo(str11)); //c和C差32 c比C大32
//7.比较字符串大小【不区分大小写】
System.out.println(str.compareToIgnoreCase(str11)); //0 不区分大小写就是一个字符串

//8.将字符串的大写字母转为小写
System.out.println(str1.toLowerCase()); // ABCDEF --> abcdef
//9.将字符串的小写字母转为大写
System.out.println(str.toUpperCase()); // abcdef --> ABCDEF

//10.去掉字符串前后空白符
System.out.println(str111.trim()); //afs asd
//11.结果在常量池中共享
System.out.println(str.concat(str1));//abcdefABCDEF 存储在字符串常量池
}
}

1.4.2 系列2:查找

(11)boolean contains(xx):是否包含xx
(12)int indexOf(xx):从前往后找当前字符串中xx,即如果有返回第一次出现的下标,要是没有返回-1
(13)int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始
(14)int lastIndexOf(xx):从后往前找当前字符串中xx,即如果有返回最后一次出现的下标,要是没有返回-1
(15)int lastIndexOf(String str, int fromIndex):返回指定子字符串在此字符串中最后一次出现的索引,从指定的索引开始反向搜索。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class StringMethod2 {
public static void main(String[] args) {
String str="afasfawfqrasf";
String str1="fqr"; //包含的
String str11="asf"; //包含的
String str2="fqa"; //不包含的
//11.是否包含xx
System.out.println(str.contains(str1)); //包含了str1 true
System.out.println(str.contains("aaa")); //不包含aaa false

//从前往后
//12.查找xx在此字符串中第一次出现的索引【可以从指定索引开始】
System.out.println(str.indexOf(str11)); //找到了 2 [第一次出现]
//13.查找xx在此字符串中第一次出现的索引【从指定索引开始】
System.out.println(str.indexOf(str11,8)); //找到了 10 [第一次出现]

//从后往前
//14.查找xx在此字符串中最后一次出现的索引
System.out.println(str.lastIndexOf(str11)); //找到了 10 [从后往前的最后一次 == 第一次出现]
//15.查找xx在此字符串中最后一次出现的下标【从指定索引反向搜索】
System.out.println(str.lastIndexOf(str11,9)); //找到了 2 [从后往前的最后一次 == 第一次出现]

}
}

1.4.3 系列3:字符串截取

(16)String substring(int beginIndex) :返回一个新的字符串,它是此字符串的从beginIndex开始截取到最后的一个子字符串。
(17)String substring(int beginIndex, int endIndex) :返回一个新字符串,它是此字符串从beginIndex开始截取到endIndex(不包含)的一个子字符串。

1
2
3
4
5
6
7
8
9
public class StringMethod3 {
public static void main(String[] args) {
String str="asdasfdewrthfdg中国asda北京";
//16.截取从(beginIndex-字符串结尾)的部分
System.out.println(str.substring(5)); //fdewrthfdg中国asda北京
//17.截取从(beginIndex-endIndex)部分
System.out.println(str.substring(5,16)); //fdewrthfdg中
}
}

1.4.4 系列4:和字符/字符数组相关

(18)char charAt(index):返回[index]位置的字符
(19)char[] toCharArray(): 将此字符串转换为一个新的字符数组返回
(20)static String valueOf(char[] data) :返回指定数组中表示该字符序列的 String
(21)static String valueOf(char[] data, int offset, int count) : 返回指定数组中表示该字符序列的 String
(22)static String copyValueOf(char[] data): 返回指定数组中表示该字符序列的 String
(23)static String copyValueOf(char[] data, int offset, int count):返回指定数组中表示该字符序列的 String

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class StringMethod4 {
public static void main(String[] args) {
String str="asfegewgsx北京中萨芬sasdafe";
//18.返回指定index位置的字符
System.out.println(str.charAt(11)); //京
//19.字符串 --> char[]
char[] arr=str.toCharArray(); //转为一个char数组
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]+" "); //a s f e g e w g s x 北 京 中 萨 芬 s a s d a f e
}
System.out.println();

//以下四个为静态方法 使用String类调用 --都是将字符数组/字符数组的一部分转为字符串
//20.char[] --> 字符串 [取所有字符转为字符串]
System.out.println(String.valueOf(arr)); //asfegewgsx北京中萨芬sasdafe
//21.char[] --> 字符串 [取指定范围的字符转为字符串]
System.out.println(String.valueOf(arr,2,15)); //fegewgsx北京中萨芬sa

//22.char[] --> 字符串
System.out.println(String.copyValueOf(arr)); //asfegewgsx北京中萨芬sasdafe
//23.char[] --> 字符串
System.out.println(String.copyValueOf(arr,2,15)); //fegewgsx北京中萨芬sa
}
}

1.4.5 系列5:开头与结尾

(24)boolean startsWith(xx):测试此字符串是否以指定的前缀开始
(25)boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的子字符串是否以指定前缀开始
(26)boolean endsWith(xx):测试此字符串是否以指定的后缀结束

1
2
3
4
5
6
7
8
9
10
11
12
13
public class StringMethod5 {
public static void main(String[] args) {
String str="1231sdfsdt34t中国按时发放qrqasd";
//24.测试此字符串是否以指定前缀开始
System.out.println(str.startsWith("123")); //true
System.out.println(str.startsWith("124")); //false
//25.测试此字符串是否以【指定下标开始的】前缀开始
System.out.println(str.startsWith("123",1)); //false
System.out.println(str.startsWith("中国",13)); //true
//26.测试此字符串是否以指定的后缀结束
System.out.println(str.endsWith("asd")); //true
}
}

1.4.6 系列6:替换

(27)String replace(char oldChar, char newChar):通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。 不支持正则。
(28)String replace(CharSequence target, CharSequence replacement):使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串。
(29)String replaceAll(String regex, String replacement):使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。
(30)String replaceFirst(String regex, String replacement):使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class StringMethod6 {
public static void main(String[] args) {
String str="asfdewwegsdn 中国阿三大王负数saqwerqfsddds";
//不可以使用正则表达式!!!!!!!!
//替换字符
//27.将字符串中所有d字符用*字符替换
System.out.println(str.replace('d','*')); //asf*ewwegs*n 中国阿三大王负数saqwerqfs***s
//替换一部分字符串
//28.将字符串中所有的ddd子字符串用哈哈哈替换
System.out.println(str.replace("ddd","哈哈哈")); //asfdewwegsdn 中国阿三大王负数saqwerqfs哈哈哈s

//可以使用正则表达式!!!!!!!!!!
//29.字符串每一位都按照正则表达式匹配一下,不符合的用*替换掉
System.out.println(str.replaceAll("[^a-zA-Z]","*")); //asfdewwegsdn*********saqwerqfsddds
//30.字符串每一位都按照正则表达式匹配一下,只是把不符合的第一位用%替换
System.out.println(str.replaceFirst("[^a-zA-Z]","%")); //asfdewwegsdn%中国阿三大王负数saqwerqfsddds
}
}

1.5 常见算法题目

题目1:模拟一个trim方法,去除字符串两端的空格。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class StringMethod6 {
public static void main(String[] args) {
String str=" asdasrqwe 中国1234019*(*)(8 ";
System.out.println("原样为:");
System.out.println(str);
System.out.println("------------------");

//方法一:使用trim()
System.out.println("方法一之后:");
System.out.println(str.trim());
System.out.println("------------------");

//方法二:找到开头和结尾不是空格的位置,然后使用subString()截取字符串
int start=0;
int end=str.length()-1;
//找到首部第一个不为空格的下标
while(start<end){
if(str.charAt(start)==' '){
start++;
}else{
break; //找到了就break
}
}
//找到尾部第一个不为空格的下标
while(start<end){
if(str.charAt(end)==' '){
end--;
}else{
break; //找到了就break
}
}
System.out.println("方法二之后:");
System.out.println(str.substring(start,end+1)); //截取[start,end+1]部分的字符串
}
}
最终输出:
原样为:
asdasrqwe 中国1234019*(*)(8
------------------
方法一之后:
asdasrqwe 中国1234019*(*)(8
------------------
方法二之后:
asdasrqwe 中国1234019*(*)(8

题目2:将一个字符串进行反转。将字符串中指定部分进行反转。比如“abcdefg”反转为”abfedcg”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class StringMethod6 {
public static void main(String[] args) {
String str="abcdefg";
StringMethod6 m6=new StringMethod6();
System.out.println("原始字符串为:"+str);
System.out.println("-------------------------------");
//反转整个字符串
System.out.println("反转整个字符串:");
System.out.println(m6.reverseAll(str));
System.out.println("-------------------------------");
//反转字符串中指定部分
System.out.println("反转字符串中指定部分:");
System.out.println(m6.reversePart(str,2,5));
}

//反转整个字符串
public String reverseAll(String str){
String str1="";
//字符串转为字符数组
char[] arr=str.toCharArray();
//设置首部和尾部以及中间位置
int left=0;
int right=arr.length-1;
while(left<=right){
int mid=(left+right)/2;
//替换
char temp=' ';
temp=arr[left];
arr[left]=arr[right];
arr[right]=temp;
//前后指针更新
left++;
right--;
}
return new String(arr); //char[]数组转为String字符串
}

//反转字符串中指定部分
public String reversePart(String str,int start,int end){
//截取出来要反转的部分
String str1=str.substring(start,end+1);
//反转之后的结果
String str2=reverseAll(str1); //调用反转字符串的方法
//添加不用反转的部分
String str3=str.substring(0,start)+str2+str.substring(end+1,str.length()); //拼接一下
return str3;
}

}
最终输出:
原始字符串为:abcdefg
-------------------------------
反转整个字符串:
gfedcba
-------------------------------
反转字符串中指定部分:
abfedcg

题目3:获取一个字符串在另一个字符串中出现的次数。
比如:获取“ ab”在 “abkkcadkabkebfkabkskab” 中出现的次数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class StringMethod6 {
public static void main(String[] args) {
String str1 = "cdabkkcadkabkebfkabkskab";
String str2 = "ab";
System.out.println("出现的次数是:"+getCount(str1, str2));

}

//关键点就是根据indexOf(String s,int index)方法找到第一次出现的位置 这样方便index进行更替!!!!
private static int getCount(String str1, String str2) {
//获取出现次数
int num=0;
//下标
int index=0;
//找到一次就把判断的范围缩小一点
//str1.indexOf(str2,index) 返回str1从(index,str1.length())范围内第一次出现str2的下标
while((index=str1.indexOf(str2,index))!=-1){ //返回str2第一次出现的位置
index+=str2.length(); //判断范围缩小 Index下标往后挪动str2的长度这么多
System.out.println("下一轮的寻找范围从下标"+index+"开始判断");
num++; //while成功一次说明范围内还有 也就是这次while已经算一次
}
return num;
}

}
最终输出:
下一轮的寻找范围从下标4开始判断b
下一轮的寻找范围从下标12开始判断b
下一轮的寻找范围从下标19开始判断b
下一轮的寻找范围从下标24开始判断b
出现的次数是:4

题目4:获取两个字符串中最大相同子串。比如:
str1 = “abcwerthelloyuiodef“;str2 = “cvhellobnm”
提示:将短的那个串进行长度依次递减的子串与较长的串比较。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
	// 第4题
// 如果只存在一个最大长度的相同子串
public String getMaxSameSubString(String str1, String str2) {
if (str1 != null && str2 != null) {
String maxStr = (str1.length() > str2.length()) ? str1 : str2;
String minStr = (str1.length() > str2.length()) ? str2 : str1;
int len = minStr.length();
for (int i = 0; i < len; i++) {// 0 1 2 3 4 此层循环决定要去几个字符
for (int x = 0, y = len - i; y <= len; x++, y++) {
if (maxStr.contains(minStr.substring(x, y))) { //看看str长是否有str短
return minStr.substring(x, y); //如果有的话就返回str短的具体长度
}
}
}
}
return null;
}

// 如果存在多个长度相同的最大相同子串
// 此时先返回String[],后面可以用集合中的ArrayList替换,较方便
public String[] getMaxSameSubString1(String str1, String str2) {
if (str1 != null && str2 != null) {
StringBuffer sBuffer = new StringBuffer();
String maxString = (str1.length() > str2.length()) ? str1 : str2;
String minString = (str1.length() > str2.length()) ? str2 : str1;

int len = minString.length();
for (int i = 0; i < len; i++) {
for (int x = 0, y = len - i; y <= len; x++, y++) {
String subString = minString.substring(x, y);
if (maxString.contains(subString)) {
sBuffer.append(subString + ",");
}
}
System.out.println(sBuffer);
if (sBuffer.length() != 0) {
break;
}
}
String[] split = sBuffer.toString().replaceAll(",$", "").split("\\,");
return split;
}

return null;
}
// 如果存在多个长度相同的最大相同子串:使用ArrayList
// public List<String> getMaxSameSubString1(String str1, String str2) {
// if (str1 != null && str2 != null) {
// List<String> list = new ArrayList<String>();
// String maxString = (str1.length() > str2.length()) ? str1 : str2;
// String minString = (str1.length() > str2.length()) ? str2 : str1;
//
// int len = minString.length();
// for (int i = 0; i < len; i++) {
// for (int x = 0, y = len - i; y <= len; x++, y++) {
// String subString = minString.substring(x, y);
// if (maxString.contains(subString)) {
// list.add(subString);
// }
// }
// if (list.size() != 0) {
// break;
// }
// }
// return list;
// }
//
// return null;
// }

@Test
public void testGetMaxSameSubString() {
String str1 = "abcwerthelloyuiodef";
String str2 = "cvhellobnmiodef";
String[] strs = getMaxSameSubString1(str1, str2);
System.out.println(Arrays.toString(strs));
}

题目5:对字符串中字符进行自然顺序排序。
提示:
1)字符串变成字符数组。 —使用toCharArray()方法
2)对数组排序,选择,冒泡,Arrays.sort(); —使用Arrays.sort()方法
3)将排序后的数组变成字符串。 —使用new String()构造器方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class StringMethod6 {
public static void main(String[] args) {
String str1 = "cdabkkcadkabkebfkabkskab";
System.out.println("排序前:"+str1);
System.out.println("-----------------------------------------------------------------");
//1.字符串转为字符数组
char[] arr=str1.toCharArray();
System.out.println("转为char数组:");
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]+" ");
}
System.out.println();
System.out.println("-----------------------------------------------------------------");
//2.进行排序
Arrays.sort(arr);
System.out.println("排序后:"+new String(arr));
System.out.println("-----------------------------------------------------------------");
//3.转回字符串
System.out.println(new String(arr));
System.out.println("-----------------------------------------------------------------");
}
}

2. 字符串相关类之可变字符序列:StringBuffer、StringBuilder

因为String对象是不可变对象,虽然可以共享常量对象,但是对于频繁字符串的修改和拼接操作,效率极低,空间消耗也比较高。因此,JDK又在java.lang包提供了可变字符序列StringBuffer和StringBuilder类型。

2.1 StringBuffer与StringBuilder的理解

  • java.lang.StringBuffer代表可变的字符序列,JDK1.0中声明,可以对字符串内容进行增删,此时不会产生新的对象。比如:

    1
    2
    3
    4
    5
    //情况1:
    String s = new String("我喜欢学习");
    //情况2:
    StringBuffer buffer = new StringBuffer("我喜欢学习");
    buffer.append("数学");

    image-20220405221714261

image-20220228153030902
  • 继承结构:

image-20220405174233055

image-20220405174414780
  • StringBuilder 和 StringBuffer 非常类似,均代表可变的字符序列,而且提供相关功能的方法也一样。

  • 区分String、StringBuffer、StringBuilder

    • String:不可变的字符序列; 底层使用char[]数组存储(JDK8.0中)
    • StringBuffer:可变的字符序列;线程安全(方法有synchronized修饰),效率低;底层使用char[]数组存储 (JDK8.0中)
    • StringBuilder:可变的字符序列; jdk1.5引入,线程不安全的,效率高;底层使用char[]数组存储(JDK8.0中)

    image-20231023204515338

2.2 StringBuilder、StringBuffer的API

StringBuilder、StringBuffer的API是完全一致的,并且很多方法与String相同。

1、常用API

(1)StringBuffer append(xx):提供了很多的append()方法,用于进行字符串追加的方式拼接
(2)StringBuffer delete(int start, int end):删除[start,end)之间字符
(3)StringBuffer deleteCharAt(int index):删除[index]位置字符
(4)StringBuffer replace(int start, int end, String str):替换[start,end)范围的字符序列为str
(5)void setCharAt(int index, char c):替换[index]位置字符
(6)char charAt(int index):查找指定index位置上的字符
(7)StringBuffer insert(int index, xx):在[index]位置插入xx
(8)int length():返回存储的字符数据的长度
(9)StringBuffer reverse():反转

  • 当append和insert时,如果原来value数组长度不够,可扩容。

  • 如上(1)(2)(3)(4)(9)这些方法支持方法链操作。原理:

    image-20220405223542750

2、其它API

(1)int indexOf(String str):在当前字符序列中查询str的第一次出现下标
(2)int indexOf(String str, int fromIndex):在当前字符序列[fromIndex,最后]中查询str的第一次出现下标
(3)int lastIndexOf(String str):在当前字符序列中查询str的最后一次出现下标
(4)int lastIndexOf(String str, int fromIndex):在当前字符序列[fromIndex,最后]中查询str的最后一次出现下标
(5)String substring(int start):截取当前字符序列[start,最后]
(6)String substring(int start, int end):截取当前字符序列[start,end)
(7)String toString():返回此序列中数据的字符串表示形式
(8)void setLength(int newLength) :设置当前字符序列长度为newLength

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Test
public void test1(){
StringBuilder s = new StringBuilder();
s.append("hello").append(true).append('a').append(12).append("atguigu");
System.out.println(s);
System.out.println(s.length());
}

@Test
public void test2(){
StringBuilder s = new StringBuilder("helloworld");
s.insert(5, "java");
s.insert(5, "chailinyan");
System.out.println(s);
}

@Test
public void test3(){
StringBuilder s = new StringBuilder("helloworld");
s.delete(1, 3);
s.deleteCharAt(4);
System.out.println(s);
}
@Test
public void test4(){
StringBuilder s = new StringBuilder("helloworld");
s.reverse();
System.out.println(s);
}

@Test
public void test5(){
StringBuilder s = new StringBuilder("helloworld");
s.setCharAt(2, 'a');
System.out.println(s);
}

@Test
public void test6(){
StringBuilder s = new StringBuilder("helloworld");
s.setLength(30);
System.out.println(s);
}

2.3 效率测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//初始设置
long startTime = 0L;
long endTime = 0L;
String text = "";
StringBuffer buffer = new StringBuffer("");
StringBuilder builder = new StringBuilder("");

//开始对比
startTime = System.currentTimeMillis();
for (int i = 0; i < 20000; i++) {
buffer.append(String.valueOf(i));
}
endTime = System.currentTimeMillis();
System.out.println("StringBuffer的执行时间:" + (endTime - startTime));

startTime = System.currentTimeMillis();
for (int i = 0; i < 20000; i++) {
builder.append(String.valueOf(i));
}
endTime = System.currentTimeMillis();
System.out.println("StringBuilder的执行时间:" + (endTime - startTime));

startTime = System.currentTimeMillis();
for (int i = 0; i < 20000; i++) {
text = text + i;
}
endTime = System.currentTimeMillis();
System.out.println("String的执行时间:" + (endTime - startTime));

2.4 练习

笔试题:程序输出:

1
2
3
4
5
6
7
String str = null;
StringBuffer sb = new StringBuffer();
sb.append(str);
System.out.println(sb.length());//输出4
System.out.println(sb);//输出null
StringBuffer sb1 = new StringBuffer(str); //线程安全 Cannot invoke "String.length()" because "str" is null
System.out.println(sb1);//输出null

3. JDK8之前:日期时间API

3.1 java.lang.System类的方法

  • System类提供的public static long currentTimeMillis():用来返回当前时间与1970年1月1日0时0分0秒之间以毫秒为单位的时间差。

    • 此方法适于计算时间差。
  • 计算世界时间的主要标准有:

    • UTC(Coordinated Universal Time)
    • GMT(Greenwich Mean Time)
    • CST(Central Standard Time)

    在国际无线电通信场合,为了统一起见,使用一个统一的时间,称为通用协调时(UTC, Universal Time Coordinated)。UTC与格林尼治平均时(GMT, Greenwich Mean Time)一样,都与英国伦敦的本地时相同。这里,UTC与GMT含义完全相同。

3.2 java.util.Date

表示特定的瞬间,精确到毫秒。

  • 构造器:
    • Date():使用无参构造器创建的对象可以获取本地当前时间。
    • Date(long 毫秒数):把该毫秒值换算成日期时间对象
  • 常用方法
    • getTime(): 返回自 1970 年 1 月 1 日 00:00:00 GMT 以来此 Date 对象表示的毫秒数。
    • toString(): 把此 Date 对象转换为以下形式的 String: dow mon dd hh:mm:ss zzz yyyy 其中: dow 是一周中的某一天 (Sun, Mon, Tue, Wed, Thu, Fri, Sat),zzz是时间标准。
    • 其它很多方法都过时了。
  • 举例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 @Test
public void test1(){
//创建一个基于当前系统时间的Date实例
Date date=new Date();
System.out.println(date.toString()); //Tue Oct 24 10:25:37 CST 2023
System.out.println(date.getTime()); //对应的毫秒数:1698114337721
}

@Test
public void test2(){
//创建一个基于自定义时间的Date实例
//或者写成 java.sql.Date date=new java.sql.Date(date.getTime());
java.sql.Date date=new java.sql.Date(1698114337721L);
System.out.println(date.toString()); //2023-10-24
System.out.println(date.getTime()); //对应的毫秒数:1698114337721
}

3.3 java.text.SimpleDateFormat

  • java.text.SimpleDateFormat类是一个不与语言环境有关的方式来格式化和解析日期的具体类。
  • 可以进行格式化:日期 –> 文本
  • 可以进行解析:文本 –> 日期
  • 构造器:
    • SimpleDateFormat() :默认的模式和语言环境创建对象
    • public SimpleDateFormat(String pattern):该构造方法可以用参数pattern指定的格式创建一个对象
  • 格式化:
    • public String format(Date date):方法格式化时间对象date
  • 解析:
    • public Date parse(String source):从给定字符串的开始解析文本,以生成一个日期。

1572599023197

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//使用SimpleDateFormat类
@Test
public void test3() throws ParseException {
//1.没设置需要的格式
SimpleDateFormat sdf=new SimpleDateFormat();
//格式化: 日期date ---> String
Date date=new Date();
String str=sdf.format(date); //2023/10/24 上午10:42
System.out.println(str);
//解析: String ---> 日期date
Date date1=sdf.parse("2023/10/24 上午10:42");
System.out.println(date1.toString()); //Tue Oct 24 10:42:00 CST 2023
//2.设置需要的格式
SimpleDateFormat sdf1=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String str1=sdf1.format(date); //2023-10-24 10:46:37
System.out.println(str1);
}

3.4 java.util.Calendar(日历)

image-20220511105252261
  • Date类的API大部分被废弃了,替换为Calendar。

  • Calendar 类是一个抽象类,主用用于完成日期字段之间相互操作的功能。

  • 获取Calendar实例的方法

    • 使用Calendar.getInstance()方法

      image-20220123184906903

    • 调用它的子类GregorianCalendar(公历)的构造器。

      image-20220405225828816

  • 一个Calendar的实例是系统时间的抽象表示,可以修改或获取 YEAR、MONTH、DAY_OF_WEEK、HOUR_OF_DAY 、MINUTE、SECOND等 日历字段对应的时间值。

    • public int get(int field):返回给定日历字段的值
    • public void set(int field,int value) :将给定的日历字段设置为指定的值
    • public void add(int field,int amount):根据日历的规则,为给定的日历字段添加或者减去指定的时间量
    • public final Date getTime():将Calendar转成Date对象
    • public final void setTime(Date date):使用指定的Date对象重置Calendar的时间
  • 常用字段

    1620277709044

  • 注意:

    • 获取月份时:一月是0,二月是1,以此类推,12月是11
    • 获取星期时:周日是1,周二是2 , 。。。。周六是7
  • 示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import org.junit.Test;
import java.util.Calendar;
import java.util.TimeZone;

public class TestCalendar {
@Test
public void test1(){
Calendar c = Calendar.getInstance();
System.out.println(c);

int year = c.get(Calendar.YEAR);
int month = c.get(Calendar.MONTH)+1;
int day = c.get(Calendar.DATE);
int hour = c.get(Calendar.HOUR_OF_DAY);
int minute = c.get(Calendar.MINUTE);

System.out.println(year + "-" + month + "-" + day + " " + hour + ":" + minute);
}

@Test
public void test2(){
TimeZone t = TimeZone.getTimeZone("America/Los_Angeles");
Calendar c = Calendar.getInstance(t);
int year = c.get(Calendar.YEAR);
int month = c.get(Calendar.MONTH)+1;
int day = c.get(Calendar.DATE);
int hour = c.get(Calendar.HOUR_OF_DAY);
int minute = c.get(Calendar.MINUTE);

System.out.println(year + "-" + month + "-" + day + " " + hour + ":" + minute);
}

@Test
public void test3(){
Calendar calendar = Calendar.getInstance();
// 从一个 Calendar 对象中获取 Date 对象
Date date = calendar.getTime();

// 使用给定的 Date 设置此 Calendar 的时间
date = new Date(234234235235L);
calendar.setTime(date);
calendar.set(Calendar.DAY_OF_MONTH, 8);
System.out.println("当前时间日设置为8后,时间是:" + calendar.getTime());

calendar.add(Calendar.HOUR, 2);
System.out.println("当前时间加2小时后,时间是:" + calendar.getTime());

calendar.add(Calendar.MONTH, -2);
System.out.println("当前日期减2个月后,时间是:" + calendar.getTime());
}
}

3.5 练习

输入年份和月份,输出该月日历。

闰年计算公式:年份可以被4整除但不能被100整除,或者可以被400整除。

image-20220503120722810

4. JDK8:新的日期时间API

如果我们可以跟别人说:“我们在1502643933071见面,别晚了!”那么就再简单不过了。但是我们希望时间与昼夜和四季有关,于是事情就变复杂了。JDK 1.0中包含了一个java.util.Date类,但是它的大多数方法已经在JDK 1.1引入Calendar类之后被弃用了。而Calendar并不比Date好多少。它们面临的问题是:

  • 可变性:像日期和时间这样的类应该是不可变的。【Calendar类的set方法可以更改时间和日期】

  • 偏移性:Date中的年份是从1900开始的,而月份都从0开始。【具有偏移量,不能直接写2023就是2023】

  • 格式化:格式化只对Date有用,Calendar则不行。【】

  • 此外,它们也不是线程安全的;不能处理闰秒等。【】

    闰秒,是指为保持协调世界时接近于世界时时刻,由国际计量局统一规定在年底或年中(也可能在季末)对协调世界时增加或减少1秒的调整。由于地球自转的不均匀性和长期变慢性(主要由潮汐摩擦引起的),会使世界时(民用时)和原子时之间相差超过到±0.9秒时,就把协调世界时向前拨1秒(负闰秒,最后一分钟为59秒)或向后拨1秒(正闰秒,最后一分钟为61秒); 闰秒一般加在公历年末或公历六月末。

    目前,全球已经进行了27次闰秒,均为正闰秒。

总结:对日期和时间的操作一直是Java程序员最痛苦的地方之一

第三次引入的API是成功的,并且Java 8中引入的java.time 已经纠正了过去的缺陷,将来很长一段时间内它都会为我们服务。

Java 8 以一个新的开始为 Java 创建优秀的 API。新的日期时间API包含:

  • java.time – 包含值对象的基础包
  • java.time.chrono – 提供对不同的日历系统的访问。
  • java.time.format – 格式化和解析时间和日期
  • java.time.temporal – 包括底层框架和扩展特性
  • java.time.zone – 包含时区支持的类

说明:新的 java.time 中包含了所有关于时钟(Clock),本地日期(LocalDate)、本地时间(LocalTime)、本地日期时间(LocalDateTime)、时区(ZonedDateTime)和持续时间(Duration)的类。

尽管有68个新的公开类型,但是大多数开发者只会用到基础包和format包,大概占总数的三分之一。

4.1 本地日期时间:LocalDate、LocalTime、LocalDateTime

方法 描述
now()/
now(ZoneId zone)
静态方法,根据当前时间创建对象/指定时区的对象
of(xx,xx,xx,xx,xx,xxx) 静态方法,根据指定日期/时间创建对象
getDayOfMonth()/
getDayOfYear()
获得月份天数(1-31) /获得年份天数(1-366)
getDayOfWeek() 获得星期几(返回一个 DayOfWeek 枚举值)
getMonth() 获得月份, 返回一个 Month 枚举值
getMonthValue() /
getYear()
获得月份(1-12) /获得年份
getHours()/
getMinute()/
getSecond()
获得当前对象对应的小时、分钟、秒
withDayOfMonth()/
withDayOfYear()/
withMonth()/
withYear()
将月份天数、年份天数、月份、年份修改为指定的值并返回新的对象
with(TemporalAdjuster t) 将当前日期时间设置为校对器指定的日期时间
plusDays(),
plusWeeks(),
plusMonths(),
plusYears(),
plusHours()
向当前对象添加几天、几周、几个月、几年、几小时
minusMonths() /
minusWeeks()/
minusDays()/
minusYears()/
minusHours()
从当前对象减去几月、几周、几天、几年、几小时
plus(TemporalAmount t)/
minus(TemporalAmount t)
添加或减少一个 Duration 或 Period
isBefore()/
isAfter()
比较两个 LocalDate
isLeapYear() 判断是否是闰年(在LocalDate类中声明)
format(DateTimeFormatter t) 格式化本地日期、时间,返回一个字符串
parse(Charsequence text) 将指定格式的字符串解析为日期、时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
public void test3(){
//now() 获取当前日期和时间
LocalDate localDate=LocalDate.now();
System.out.println(localDate); //2023-10-24

LocalTime localTime= LocalTime.now();
System.out.println(localTime); //11:08:04.703426700

LocalDateTime localDateTime=LocalDateTime.now();
System.out.println(localDateTime); //2023-10-24T11:08:04.703426700

//of() 获取指定日期和时间 【没有偏移量,指定哪天就是哪天】
LocalDate localDate1=LocalDate.of(2023,10,13);
System.out.println(localDate1); //2023-10-13

LocalTime localTime1=LocalTime.of(12,10,24);
System.out.println(localTime1); //12:10:24

LocalDateTime localDateTime1=LocalDateTime.of(2023,10,1,12,13,1);
System.out.println(localDateTime1); //2023-10-01T12:13:01

4.2 瞬时:Instant

  • Instant:时间线上的一个瞬时点。 这可能被用来记录应用程序中的事件时间戳。[购物平台的购买记录,类似于订单号每个人只有一个]
    • 时间戳是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数。
  • java.time.Instant表示时间线上的一点,而不需要任何上下文信息,例如,时区。概念上讲,它只是简单的表示自1970年1月1日0时0分0秒(UTC)开始的秒数。
方法 描述
now() 静态方法,返回默认UTC时区的Instant类的对象
ofEpochMilli(long epochMilli) 静态方法,返回在1970-01-01 00:00:00基础上加上指定毫秒数之后的Instant类的对象
atOffset(ZoneOffset offset) 结合即时的偏移来创建一个 OffsetDateTime
toEpochMilli() 返回1970-01-01 00:00:00到当前时间的毫秒数,即为时间戳

中国大陆、中国香港、中国澳门、中国台湾、蒙古国、新加坡、马来西亚、菲律宾、西澳大利亚州的时间与UTC的时差均为+8,也就是UTC+8。

instant.atOffset(ZoneOffset.ofHours(8));

image-20220406000442908

整个地球分为二十四时区,每个时区都有自己的本地时间。北京时区是东八区,领先UTC八个小时,在电子邮件信头的Date域记为+0800。如果在电子邮件的信头中有这么一行:

Date: Fri, 08 Nov 2002 09:42:22 +0800

说明信件的发送地的地方时间是二○○二年十一月八号,星期五,早上九点四十二分(二十二秒),这个地方的本地时领先UTC八个小时(+0800, 就是东八区时间)。电子邮件信头的Date域使用二十四小时的时钟,而不使用AM和PM来标记上下午。

4.3 日期时间格式化:DateTimeFormatter

该类提供了三种格式化方法:

  • (了解)预定义的标准格式。如:ISO_LOCAL_DATE_TIME、ISO_LOCAL_DATE、ISO_LOCAL_TIME
  • (了解)本地化相关的格式。如:ofLocalizedDate(FormatStyle.LONG)

    1
    2
    3
    4
    5
    // 本地化相关的格式。如:ofLocalizedDateTime()
    // FormatStyle.MEDIUM / FormatStyle.SHORT :适用于LocalDateTime

    // 本地化相关的格式。如:ofLocalizedDate()
    // FormatStyle.FULL / FormatStyle.LONG / FormatStyle.MEDIUM / FormatStyle.SHORT : 适用于LocalDate
  • 自定义的格式。如:ofPattern(“yyyy-MM-dd hh:mm:ss”)

ofPattern(String pattern) 静态方法,返回一个指定字符串格式的DateTimeFormatter
format(TemporalAccessor t) 格式化一个日期、时间,返回字符串
parse(CharSequence text) 将指定格式的字符序列解析为一个日期、时间

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import org.junit.Test;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;

public class TestDatetimeFormatter {
@Test
public void test1(){
// 方式一:预定义的标准格式。如:ISO_LOCAL_DATE_TIME;ISO_LOCAL_DATE;ISO_LOCAL_TIME
DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
// 格式化:日期-->字符串
LocalDateTime localDateTime = LocalDateTime.now();
String str1 = formatter.format(localDateTime);
System.out.println(localDateTime);
System.out.println(str1);//2022-12-04T21:02:14.808

// 解析:字符串 -->日期
TemporalAccessor parse = formatter.parse("2022-12-04T21:02:14.808");
LocalDateTime dateTime = LocalDateTime.from(parse);
System.out.println(dateTime);
}

@Test
public void test2(){
LocalDateTime localDateTime = LocalDateTime.now();
// 方式二:
// 本地化相关的格式。如:ofLocalizedDateTime()
// FormatStyle.LONG / FormatStyle.MEDIUM / FormatStyle.SHORT :适用于LocalDateTime
DateTimeFormatter formatter1 = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG);

// 格式化
String str2 = formatter1.format(localDateTime);
System.out.println(str2);// 2022年12月4日 下午09时03分55秒

// 本地化相关的格式。如:ofLocalizedDate()
// FormatStyle.FULL / FormatStyle.LONG / FormatStyle.MEDIUM / FormatStyle.SHORT : 适用于LocalDate
DateTimeFormatter formatter2 = DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL);
// 格式化
String str3 = formatter2.format(LocalDate.now());
System.out.println(str3);// 2022年12月4日 星期日
}

@Test
public void test3(){
//方式三:自定义的方式(关注、重点)
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
//格式化
String strDateTime = dateTimeFormatter.format(LocalDateTime.now());
System.out.println(strDateTime); //2022/12/04 21:05:42
//解析
TemporalAccessor accessor = dateTimeFormatter.parse("2022/12/04 21:05:42");
LocalDateTime localDateTime = LocalDateTime.from(accessor);
System.out.println(localDateTime); //2022-12-04T21:05:42
}
}

4.4 其它API

1、指定时区日期时间:ZondId和ZonedDateTime

  • ZoneId:该类中包含了所有的时区信息,一个时区的ID,如 Europe/Paris

  • ZonedDateTime:一个在ISO-8601日历系统时区的日期时间,如 2007-12-03T10:15:30+01:00 Europe/Paris。

    • 其中每个时区都对应着ID,地区ID都为“{区域}/{城市}”的格式,例如:Asia/Shanghai等
  • 常见时区ID:

1
2
3
Asia/Shanghai
UTC
America/New_York
  • 可以通过ZondId获取所有可用的时区ID:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Set;

public class TestZone {
@Test
public void test01() {
//需要知道一些时区的id
//Set<String>是一个集合,容器
Set<String> availableZoneIds = ZoneId.getAvailableZoneIds();
//快捷模板iter
for (String availableZoneId : availableZoneIds) {
System.out.println(availableZoneId);
}
}

@Test
public void test02(){
ZonedDateTime t1 = ZonedDateTime.now();
System.out.println(t1);

ZonedDateTime t2 = ZonedDateTime.now(ZoneId.of("America/New_York"));
System.out.println(t2);
}
}

2、持续日期/时间:Period和Duration

  • 持续时间:Duration,用于计算两个“时间”间隔
  • 日期间隔:Period,用于计算两个“日期”间隔
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import org.junit.Test;

import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Period;

public class TestPeriodDuration {
@Test
public void test01(){
LocalDate t1 = LocalDate.now();
LocalDate t2 = LocalDate.of(2018, 12, 31);
Period between = Period.between(t1, t2);
System.out.println(between);

System.out.println("相差的年数:"+between.getYears());
System.out.println("相差的月数:"+between.getMonths());
System.out.println("相差的天数:"+between.getDays());
System.out.println("相差的总数:"+between.toTotalMonths());
}

@Test
public void test02(){
LocalDateTime t1 = LocalDateTime.now();
LocalDateTime t2 = LocalDateTime.of(2017, 8, 29, 0, 0, 0, 0);
Duration between = Duration.between(t1, t2);
System.out.println(between);

System.out.println("相差的总天数:"+between.toDays());
System.out.println("相差的总小时数:"+between.toHours());
System.out.println("相差的总分钟数:"+between.toMinutes());
System.out.println("相差的总秒数:"+between.getSeconds());
System.out.println("相差的总毫秒数:"+between.toMillis());
System.out.println("相差的总纳秒数:"+between.toNanos());
System.out.println("不够一秒的纳秒数:"+between.getNano());
}
@Test
public void test03(){
//Duration:用于计算两个“时间”间隔,以秒和纳秒为基准
LocalTime localTime = LocalTime.now();
LocalTime localTime1 = LocalTime.of(15, 23, 32);
//between():静态方法,返回Duration对象,表示两个时间的间隔
Duration duration = Duration.between(localTime1, localTime);
System.out.println(duration);

System.out.println(duration.getSeconds());
System.out.println(duration.getNano());

LocalDateTime localDateTime = LocalDateTime.of(2016, 6, 12, 15, 23, 32);
LocalDateTime localDateTime1 = LocalDateTime.of(2017, 6, 12, 15, 23, 32);

Duration duration1 = Duration.between(localDateTime1, localDateTime);
System.out.println(duration1.toDays());
}

@Test
public void test4(){
//Period:用于计算两个“日期”间隔,以年、月、日衡量
LocalDate localDate = LocalDate.now();
LocalDate localDate1 = LocalDate.of(2028, 3, 18);

Period period = Period.between(localDate, localDate1);
System.out.println(period);

System.out.println(period.getYears());
System.out.println(period.getMonths());
System.out.println(period.getDays());

Period period1 = period.withYears(2);
System.out.println(period1);

}
}

3、Clock:使用时区提供对当前即时、日期和时间的访问的时钟。

4、TemporalAdjuster : 时间校正器。有时我们可能需要获取例如:将日期调整到“下一个工作日”等操作。
TemporalAdjusters : 该类通过静态方法(firstDayOfXxx()/lastDayOfXxx()/nextXxx())提供了大量的常用 TemporalAdjuster 的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void test1(){
// TemporalAdjuster:时间校正器
// 获取当前日期的下一个周日是哪天?
TemporalAdjuster temporalAdjuster = TemporalAdjusters.next(DayOfWeek.SUNDAY);
LocalDateTime localDateTime = LocalDateTime.now().with(temporalAdjuster);
System.out.println(localDateTime);
// 获取下一个工作日是哪天?
LocalDate localDate = LocalDate.now().with(new TemporalAdjuster() {
@Override
public Temporal adjustInto(Temporal temporal) {
LocalDate date = (LocalDate) temporal;
if (date.getDayOfWeek().equals(DayOfWeek.FRIDAY)) {
return date.plusDays(3);
} else if (date.getDayOfWeek().equals(DayOfWeek.SATURDAY)) {
return date.plusDays(2);
} else {
return date.plusDays(1);
}
}
});
System.out.println("下一个工作日是:" + localDate);

}

4.5 与传统日期处理的转换

To 遗留类 From 遗留类
java.time.Instant与java.util.Date Date.from(instant) date.toInstant()
java.time.Instant与java.sql.Timestamp Timestamp.from(instant) timestamp.toInstant()
java.time.ZonedDateTime与java.util.GregorianCalendar GregorianCalendar.from(zonedDateTime) cal.toZonedDateTime()
java.time.LocalDate与java.sql.Time Date.valueOf(localDate) date.toLocalDate()
java.time.LocalTime与java.sql.Time Date.valueOf(localDate) date.toLocalTime()
java.time.LocalDateTime与java.sql.Timestamp Timestamp.valueOf(localDateTime) timestamp.toLocalDateTime()
java.time.ZoneId与java.util.TimeZone Timezone.getTimeZone(id) timeZone.toZoneId()
java.time.format.DateTimeFormatter与java.text.DateFormat formatter.toFormat()

5. Java比较器

我们知道基本数据类型的数据(除boolean类型外)需要比较大小的话,之间使用比较运算符即可,但是引用数据类型是不能直接使用比较运算符来比较大小的。那么,如何解决这个问题呢?

image-20220406001726285

  • 在Java中经常会涉及到对象数组的排序问题,那么就涉及到对象之间的比较问题。
  • Java实现对象排序的方式有两种:
    • 自然排序:java.lang.Comparable
    • 定制排序:java.util.Comparator

5.1 自然排序:java.lang.Comparable

  • Comparable接口强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序。
  • 实现 Comparable 的类必须实现 compareTo(Object obj)方法,两个对象即通过 compareTo(Object obj) 方法的返回值来比较大小。如果当前对象this大于形参对象obj,则返回正整数,如果当前对象this小于形参对象obj,则返回负整数,如果当前对象this等于形参对象obj,则返回零。
1
2
3
public interface Comparable{
int compareTo(Object obj); //抽象方法
}
  • 实现Comparable接口的对象列表(和数组)可以通过 Collections.sort 或 Arrays.sort进行自动排序。实现此接口的对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。

  • 对于类 C 的每一个 e1 和 e2 来说,当且仅当 e1.compareTo(e2) == 0 与 e1.equals(e2) 具有相同的 boolean 值时,类 C 的自然排序才叫做与 equals 一致。建议(虽然不是必需的)最好使自然排序与 equals 一致

  • Comparable 的典型实现:(默认都是从小到大排列的)

    • String:按照字符串中字符的Unicode值进行比较
    • Character:按照字符的Unicode值来进行比较
    • 数值类型对应的包装类以及BigInteger、BigDecimal:按照它们对应的数值大小进行比较
    • Boolean:true 对应的包装类实例大于 false 对应的包装类实例
    • Date、Time等:后面的日期时间比前面的日期时间大
  • 代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class Student implements Comparable {
private int id;
private String name;
private int score;
private int age;

public Student(int id, String name, int score, int age) {
this.id = id;
this.name = name;
this.score = score;
this.age = age;
}

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

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

public int getScore() {
return score;
}

public void setScore(int score) {
this.score = score;
}

public int getAge() {
return age;
}

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

@Override
public String toString() {
return "Student{" +"id=" + id +", name='" + name + '\'' +", score=" + score +", age=" + age +'}';
}
//重写compareTo方法
@Override
public int compareTo(Object o) {
//这些需要强制,将o对象向下转型为Student类型的变量,才能调用Student类中的属性
//默认按照学号比较大小
Student stu = (Student) o;
return this.id - stu.id;
}
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class TestStudent {
public static void main(String[] args) {
Student[] arr = new Student[5];
arr[0] = new Student(3,"张三",90,23);
arr[1] = new Student(1,"熊大",100,22);
arr[2] = new Student(5,"王五",75,25);
arr[3] = new Student(4,"李四",85,24);
arr[4] = new Student(2,"熊二",85,18);

//单独比较两个对象
System.out.println(arr[0].compareTo(arr[1])); //调用Person重写的方法
System.out.println(arr[1].compareTo(arr[2])); //调用Person重写的方法
System.out.println(arr[2].compareTo(arr[2])); //调用Person重写的方法

System.out.println("所有学生:");
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
System.out.println("按照学号排序:");
for (int i = 1; i < arr.length; i++) {
for (int j = 0; j < arr.length-i; j++) {
if(arr[j].compareTo(arr[j+1])>0){ //调用Person重写的方法
Student temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
//遍历数组所有元素
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
}

image-20231024154426685

5.2 定制排序:java.util.Comparator

  • 思考
    • 当元素的类型没有实现java.lang.Comparable接口而又不方便修改代码(例如:一些第三方的类,你只有.class文件,没有源文件)
    • 如果一个类,实现了Comparable接口,也指定了两个对象的比较大小的规则,但是此时此刻我不想按照它预定义的方法比较大小,但是我又不能随意修改,因为会影响其他地方的使用,怎么办?
  • JDK在设计类库之初,也考虑到这种情况,所以又增加了一个java.util.Comparator接口。强行对多个对象进行整体排序的比较。
    • 重写compare(Object o1,Object o2)方法,比较o1和o2的大小:如果方法返回正整数,则表示o1大于o2;如果返回0,表示相等;返回负整数,表示o1小于o2。
    • 可以将 Comparator 传递给 sort 方法(如 Collections.sort 或 Arrays.sort),从而允许在排序顺序上实现精确控制。
1
2
3
public interface Comparator{
int compare(Object o1,Object o2);
}

举例:

1
2
3
4
5
6
7
8
9
10
11
import java.util.Comparator;
//定义定制比较器类
public class StudentScoreComparator implements Comparator {
@Override
public int compare(Object o1, Object o2) {
Student s1 = (Student) o1;
Student s2 = (Student) o2;
int result = s1.getScore() - s2.getScore();
return result != 0 ? result : s1.getId() - s2.getId();
}
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class TestStudent {
public static void main(String[] args) {
Student[] arr = new Student[5];
arr[0] = new Student(3, "张三", 90, 23);
arr[1] = new Student(1, "熊大", 100, 22);
arr[2] = new Student(5, "王五", 75, 25);
arr[3] = new Student(4, "李四", 85, 24);
arr[4] = new Student(2, "熊二", 85, 18);


System.out.println("所有学生:");
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}

System.out.println("按照成绩排序");
StudentScoreComparator sc = new StudentScoreComparator();
for (int i = 1; i < arr.length; i++) {
for (int j = 0; j < arr.length - i; j++) {
if (sc.compare(arr[j], arr[j + 1]) > 0) {
Student temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
}

再举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void test01() {
Student[] students = new Student[5];
students[0] = new Student(3, "张三", 90, 23);
students[1] = new Student(1, "熊大", 100, 22);
students[2] = new Student(5, "王五", 75, 25);
students[3] = new Student(4, "李四", 85, 24);
students[4] = new Student(2, "熊二", 85, 18);

System.out.println(Arrays.toString(students));
//定制排序
StudentScoreComparator sc = new StudentScoreComparator();
Arrays.sort(students, sc);
System.out.println("排序之后:");
System.out.println(Arrays.toString(students));
}

再举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Goods[] all = new Goods[4];
all[0] = new Goods("War and Peace", 100);
all[1] = new Goods("Childhood", 80);
all[2] = new Goods("Scarlet and Black", 140);
all[3] = new Goods("Notre Dame de Paris", 120);

Arrays.sort(all, new Comparator() {
@Override
public int compare(Object o1, Object o2) {
Goods g1 = (Goods) o1;
Goods g2 = (Goods) o2;

return g1.getName().compareTo(g2.getName());
}
});
System.out.println(Arrays.toString(all));

6. 系统相关类

6.1 java.lang.System类

  • System类代表系统,系统级的很多属性和控制方法都放置在该类的内部。该类位于java.lang包

  • 由于该类的构造器是private的,所以无法创建该类的对象。其内部的成员变量和成员方法都是static的,所以也可以很方便的进行调用。

  • 成员变量 Scanner scan = new Scanner(System.in);

    • System类内部包含inouterr三个成员变量,分别代表标准输入流(键盘输入),标准输出流(显示器)和标准错误输出流(显示器)。
  • 成员方法

    • native long currentTimeMillis()
      该方法的作用是返回当前的计算机时间,时间的表达格式为当前计算机时间和GMT时间(格林威治时间)1970年1月1号0时0分0秒所差的毫秒数。

    • void exit(int status)
      该方法的作用是退出程序。其中status的值为0代表正常退出,非零代表异常退出。使用该方法可以在图形界面编程中实现程序的退出功能等。

    • void gc()
      该方法的作用是请求系统进行垃圾回收。至于系统是否立刻回收,则取决于系统中垃圾回收算法的实现以及系统执行时的情况。

    • String getProperty(String key)
      该方法的作用是获得系统中属性名为key的属性对应的值。系统中常见的属性名以及属性的作用如下表所示:

      image-20220406003340258

  • 举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public class TestSystem {
@Test
public void test01(){
long time = System.currentTimeMillis();
System.out.println("现在的系统时间距离1970年1月1日凌晨:" + time + "毫秒");

System.exit(0);

System.out.println("over");//不会执行
}

@Test
public void test02(){
String javaVersion = System.getProperty("java.version");
System.out.println("java的version:" + javaVersion);

String javaHome = System.getProperty("java.home");
System.out.println("java的home:" + javaHome);

String osName = System.getProperty("os.name");
System.out.println("os的name:" + osName);

String osVersion = System.getProperty("os.version");
System.out.println("os的version:" + osVersion);

String userName = System.getProperty("user.name");
System.out.println("user的name:" + userName);

String userHome = System.getProperty("user.home");
System.out.println("user的home:" + userHome);

String userDir = System.getProperty("user.dir");
System.out.println("user的dir:" + userDir);
}

@Test
public void test03() throws InterruptedException {
for (int i=1; i <=10; i++){
MyDemo my = new MyDemo(i);
//每一次循环my就会指向新的对象,那么上次的对象就没有变量引用它了,就成垃圾对象
}

//为了看到垃圾回收器工作,我要加下面的代码,让main方法不那么快结束,因为main结束就会导致JVM退出,GC也会跟着结束。
System.gc();//如果不调用这句代码,GC可能不工作,因为当前内存很充足,GC就觉得不着急回收垃圾对象。
//调用这句代码,会让GC尽快来工作。
Thread.sleep(5000);
}
}

class MyDemo{
private int value;

public MyDemo(int value) {
this.value = value;
}

@Override
public String toString() {
return "MyDemo{" + "value=" + value + '}';
}

//重写finalize方法,让大家看一下它的调用效果
@Override
protected void finalize() throws Throwable {
// 正常重写,这里是编写清理系统内存的代码
// 这里写输出语句是为了看到finalize()方法被调用的效果
System.out.println(this+ "轻轻的我走了,不带走一段代码....");
}
}
  • static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)

    从指定源数组中复制一个数组,复制从指定的位置开始,到目标数组的指定位置结束。常用于数组的插入和删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import org.junit.Test;
import java.util.Arrays;
public class TestSystemArrayCopy {
@Test
public void test01(){
int[] arr1 = {1,2,3,4,5};
int[] arr2 = new int[10];
System.arraycopy(arr1,0,arr2,3,arr1.length);
System.out.println(Arrays.toString(arr1));
System.out.println(Arrays.toString(arr2));
}

@Test
public void test02(){
int[] arr = {1,2,3,4,5};
System.arraycopy(arr,0,arr,1,arr.length-1);
System.out.println(Arrays.toString(arr));
}

@Test
public void test03(){
int[] arr = {1,2,3,4,5};
System.arraycopy(arr,1,arr,0,arr.length-1);
System.out.println(Arrays.toString(arr));
}
}

6.2 java.lang.Runtime类

每个 Java 应用程序都有一个 Runtime 类实例,使应用程序能够与其运行的环境相连接。

public static Runtime getRuntime(): 返回与当前 Java 应用程序相关的运行时对象。应用程序不能创建自己的 Runtime 类实例。

public long totalMemory():返回 Java 虚拟机中初始化时的内存总量。此方法返回的值可能随时间的推移而变化,这取决于主机环境。默认为物理电脑内存的1/64。

public long maxMemory():返回 Java 虚拟机中最大程度能使用的内存总量。默认为物理电脑内存的1/4。

public long freeMemory():回 Java 虚拟机中的空闲内存量。调用 gc 方法可能导致 freeMemory 返回值的增加。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestRuntime {
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
long initialMemory = runtime.totalMemory(); //获取虚拟机初始化时堆内存总量
long maxMemory = runtime.maxMemory(); //获取虚拟机最大堆内存总量
String str = "";
//模拟占用内存
for (int i = 0; i < 10000; i++) {
str += i;
}
long freeMemory = runtime.freeMemory(); //获取空闲堆内存总量
System.out.println("总内存:" + initialMemory / 1024 / 1024 * 64 + "MB");
System.out.println("总内存:" + maxMemory / 1024 / 1024 * 4 + "MB");
System.out.println("空闲内存:" + freeMemory / 1024 / 1024 + "MB") ;
System.out.println("已用内存:" + (initialMemory-freeMemory) / 1024 / 1024 + "MB");
}
}

7. 和数学相关的类

7.1 java.lang.Math

java.lang.Math 类包含用于执行基本数学运算的方法,如初等指数、对数、平方根和三角函数。类似这样的工具类,其所有方法均为静态方法,并且不会创建对象,调用起来非常简单。

  • public static double abs(double a) :返回 double 值的绝对值。
1
2
double d1 = Math.abs(-5); //d1的值为5
double d2 = Math.abs(5); //d2的值为5
  • public static double ceil(double a) :返回大于等于参数的最小的整数。
1
2
3
double d1 = Math.ceil(3.3); //d1的值为 4.0
double d2 = Math.ceil(-3.3); //d2的值为 -3.0
double d3 = Math.ceil(5.1); //d3的值为 6.0
  • public static double floor(double a) :返回小于等于参数最大的整数。
1
2
3
double d1 = Math.floor(3.3); //d1的值为3.0
double d2 = Math.floor(-3.3); //d2的值为-4.0
double d3 = Math.floor(5.1); //d3的值为 5.0
  • public static long round(double a) :返回最接近参数的 long。(相当于四舍五入方法)
1
2
3
4
long d1 = Math.round(5.5); //d1的值为6
long d2 = Math.round(5.4); //d2的值为5
long d3 = Math.round(-3.3); //d3的值为-3
long d4 = Math.round(-3.8); //d4的值为-4
  • public static double pow(double a,double b):返回a的b幂次方法
  • public static double sqrt(double a):返回a的平方根
  • public static double random():返回[0,1)的随机值
  • public static final double PI:返回圆周率
  • public static double max(double x, double y):返回x,y中的最大值
  • public static double min(double x, double y):返回x,y中的最小值
  • 其它:acos,asin,atan,cos,sin,tan 三角函数
1
2
3
4
double result = Math.pow(2,31);
double sqrt = Math.sqrt(256);
double rand = Math.random();
double pi = Math.PI;

7.2 java.math包

7.2.1 BigInteger

  • Integer类作为int的包装类,能存储的最大整型值为2^31-1,Long类也是有限的,最大为2^63-1。如果要表示再大的整数,不管是基本数据类型还是他们的包装类都无能为力,更不用说进行运算了。
  • java.math包的BigInteger可以表示不可变的任意精度的整数。BigInteger 提供所有 Java 的基本整数操作符的对应物,并提供 java.lang.Math 的所有相关方法。另外,BigInteger 还提供以下运算:模算术、GCD 计算、质数测试、素数生成、位操作以及一些其他操作。
  • 构造器

    • BigInteger(String val):根据字符串构建BigInteger对象
  • 方法

    • public BigInteger abs():返回此 BigInteger 的绝对值的 BigInteger。
    • BigInteger add(BigInteger val) :返回其值为 (this + val) 的 BigInteger
    • BigInteger subtract(BigInteger val) :返回其值为 (this - val) 的 BigInteger
    • BigInteger multiply(BigInteger val) :返回其值为 (this * val) 的 BigInteger
    • BigInteger divide(BigInteger val) :返回其值为 (this / val) 的 BigInteger。整数相除只保留整数部分。
    • BigInteger remainder(BigInteger val) :返回其值为 (this % val) 的 BigInteger。
    • BigInteger[] divideAndRemainder(BigInteger val):返回包含 (this / val) 后跟 (this % val) 的两个 BigInteger 的数组。
    • BigInteger pow(int exponent) :返回其值为 (this^exponent) 的 BigInteger。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.math.BigInteger;
public class BigIntegerTest {
public static void main(String[] args) {
BigInteger big1=new BigInteger("-112314124");
BigInteger big2=new BigInteger("1112414");
//1.abs() BigInteger对象取绝对值
System.out.println(big1.abs()); //112314124
//2.add(big2) 两个BigInteger对象相加
System.out.println(big1.add(big2)); //-111201710
//3.subtract(big2) 两个BigInteger对象相减
System.out.println(big1.subtract(big2)); //-11342653
//4.multiply(big2) 两个BigInteger对象相乘
System.out.println(big1.multiply(big2)); //-124939803935336

//5.divide(big2) 两个BigInteger对象相除 [只保留整数部分]
System.out.println(big1.divide(big2)); //-100
//6.remainder(big2) 两个BigInteger对象相除 [取余数]
System.out.println(big1.remainder(big2)); //-1072724
//7.divideAndremainder(big2) 两个BigInteger对象相除 将5和6方法的结果放在数组
BigInteger[] arr=big1.divideAndRemainder(big2);
System.out.println(arr[0]); //-100
System.out.println(arr[1]); //-1072724

//8.pow(x) 将BigInteger对象^X
System.out.println(big1.pow(2)); //12614462449887376
}
}
最终输出:
112314124
-111201710
-113426538
-124939803935336
-100
-1072724
-100
-1072724
12614462449887376

7.2.2 BigDecimal

  • 一般的Float类和Double类可以用来做科学计算或工程计算,但在商业计算中,要求数字精度比较高,故用到java.math.BigDecimal类。

  • BigDecimal类支持不可变的、任意精度的有符号十进制定点数。

  • 构造器

    • public BigDecimal(double val)
    • public BigDecimal(String val) –> 推荐
  • 常用方法

    • public BigDecimal add(BigDecimal augend)
    • public BigDecimal subtract(BigDecimal subtrahend)
    • public BigDecimal multiply(BigDecimal multiplicand)
    • public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode):divisor是除数,scale指明保留几位小数,roundingMode指明舍入模式(ROUND_UP :向上加1、ROUND_DOWN :直接舍去、ROUND_HALF_UP:四舍五入)
  • 举例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.math.BigDecimal;
public class BigDecimalTest {
public static void main(String[] args) {
BigDecimal big1=new BigDecimal("123132");
BigDecimal big2=new BigDecimal("100");
//1.add(big2) 两个BigDecimal类对象相加
System.out.println(big1.add(big2)); //123232
//2.subtract(big2) 两个BigDecimal类对象相加
System.out.println(big1.subtract(big2)); //123032
//3.multiply(big2) 两个BigDecimal类对象相乘
System.out.println(big1.multiply(big2)); //12313200
//4.divide(big2,3,ROUND_UP) 两个BigDecimal类对象相乘,结果保留3位,四舍五入的模式设定为向上加1
System.out.println(big1.divide(big2,3,BigDecimal.ROUND_UP)); //1231.320
}
}

7.3 java.util.Random

用于产生随机数

  • boolean nextBoolean():返回下一个伪随机数,它是取自此随机数生成器序列的均匀分布的 boolean 值。

  • void nextBytes(byte[] bytes):生成随机字节并将其置于用户提供的 byte 数组中。

  • double nextDouble():返回下一个伪随机数,它是取自此随机数生成器序列的、在 0.0 和 1.0 之间均匀分布的 double 值。

  • float nextFloat():返回下一个伪随机数,它是取自此随机数生成器序列的、在 0.0 和 1.0 之间均匀分布的 float 值。

  • double nextGaussian():返回下一个伪随机数,它是取自此随机数生成器序列的、呈高斯(“正态”)分布的 double 值,其平均值是 0.0,标准差是 1.0。

  • int nextInt():返回下一个伪随机数,它是此随机数生成器的序列中均匀分布的 int 值。

  • int nextInt(int n):返回一个伪随机数,它是取自此随机数生成器序列的、在 0(包括)和指定值(不包括)之间均匀分布的 int 值。

  • long nextLong():返回下一个伪随机数,它是取自此随机数生成器序列的均匀分布的 long 值。

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.util.Random;
public class RandomTest {
public static void main(String[] args) {
Random r=new Random();
System.out.println("随机整数:" + r.nextInt());
System.out.println("随机小数:" + r.nextDouble());
System.out.println("随机布尔值:" + r.nextBoolean());
}
}
最终输出:
随机整数:-270071674
随机小数:0.195118491954403
随机布尔值:true

JAVA-异常处理

第09章_异常处理


本章专题与脉络

第3阶段:Java高级应用-第09章

1. 异常概述

1.1 什么是生活的异常

男主角小明每天开车上班,正常车程1小时。但是,不出意外的话,可能会出现意外。

image-20220814203918560

出现意外,即为异常情况。我们会做相应的处理。如果不处理,到不了公司。处理完了,就可以正常开车去公司。

image-20220520210320301

1.2 什么是程序的异常

在使用计算机语言进行项目开发的过程中,即使程序员把代码写得尽善尽美,在系统的运行过程中仍然会遇到一些问题,因为很多问题不是靠代码能够避免的,比如:客户输入数据的格式问题读取文件是否存在网络是否始终保持通畅等等。

  • 异常 :指的是程序在执行过程中,出现的非正常情况,如果不处理最终会导致JVM的非正常停止。

异常指的并不是语法错误和逻辑错误。
1.1 语法错误,编译不通过,不会产生字节码文件,根本不能运行。

1.2 代码逻辑错误,只是没有得到想要的结果,例如:求a与b的和,你写成了a-b

1.3 异常的抛出机制

Java中是如何表示不同的异常情况,又是如何让程序员得知,并处理异常的呢?

Java中把不同的异常用不同的类表示,一旦发生某种异常,就创建该异常类型的对象,并且抛出(throw)

1.1 然后程序员可以捕获(catch)到这个异常对象,并处理;
1.2 如果没有捕获(catch)这个异常对象,那么这个异常对象将会导致程序终止。

举例:

运行下面的程序,程序会产生一个数组角标越界异常ArrayIndexOfBoundsException。我们通过图解来解析下异常产生和抛出的过程。

1
2
3
4
5
6
7
public class ArrayTools {
// 对给定的数组通过给定的角标获取元素。
public static int getElement(int[] arr, int index) {
int element = arr[index];
return element;
}
}

测试类

1
2
3
4
5
6
7
8
public class ExceptionDemo {
public static void main(String[] args) {
int[] arr = { 34, 12, 67 };
intnum = ArrayTools.getElement(arr, 4)
System.out.println("num=" + num);
System.out.println("over");
}
}

上述程序执行过程图解:

1.4 如何对待异常

对于程序出现的异常,一般有两种解决方法:一是遇到错误就终止程序的运行。另一种方法是程序员在编写程序时,就充分考虑到各种可能发生的异常和错误,极力预防和避免。实在无法避免的,要编写相应的代码进行异常的检测、以及异常的处理,保证代码的健壮性

2. Java异常体系

2.1 Throwable

java.lang.Throwable 类是Java程序执行过程中发生的异常事件对应的类的根父类。

Throwable中的常用方法:

  • public void printStackTrace():打印异常的详细信息。

    包含了异常的类型、异常的原因、异常出现的位置、在开发和调试阶段都得使用printStackTrace。

  • public String getMessage():获取发生异常的原因。

2.2 Error 和 Exception

Throwable可分为两类:Error和Exception。分别对应着java.lang.Errorjava.lang.Exception两个类。

Error:Java虚拟机无法解决的严重问题。如:JVM系统内部错误、资源耗尽等严重情况。一般不编写针对性的代码进行处理。

  • 例如:StackOverflowError(栈内存溢出)和OutOfMemoryError(堆内存溢出,简称OOM)。

Exception: 其它因编程错误或偶然的外在因素导致的一般性问题,需要使用针对性的代码进行处理,使程序继续运行。否则一旦发生异常,程序也会挂掉。例如:

  • 空指针访问
  • 试图读取不存在的文件
  • 网络连接中断
  • 数组角标越界

说明:

  1. 无论是Error还是Exception,还有很多子类,异常的类型非常丰富。当代码运行出现异常时,特别是我们不熟悉的异常时,不要紧张,把异常的简单类名,拷贝到API中去查去认识它即可。
  2. 我们本章讲的异常处理,其实针对的就是Exception。
image-20220511161910951

2.3 编译时异常和运行时异常

Java程序的执行分为编译时过程和运行时过程。有的错误只有在运行时才会发生。比如:除数为0,数组下标越界等。

image-20220330002449526

因此,根据异常可能出现的阶段,可以将异常分为:

  • 编译时期异常(即checked异常、受检异常):在代码编译阶段,编译器就能明确警示当前代码可能发生(不是一定发生)xx异常,并明确督促程序员提前编写处理它的代码。如果程序员没有编写对应的异常处理代码,则编译器就会直接判定编译失败,从而不能生成字节码文件。通常,这类异常的发生不是由程序员的代码引起的,或者不是靠加简单判断就可以避免的,例如:FileNotFoundException(文件找不到异常)。
  • 运行时期异常(即runtime异常、unchecked异常、非受检异常):在代码编译阶段,编译器完全不做任何检查,无论该异常是否会发生,编译器都不给出任何提示。只有等代码运行起来并确实发生了xx异常,它才能被发现。通常,这类异常是由程序员的代码编写不当引起的,只要稍加判断,或者细心检查就可以避免。
    • java.lang.RuntimeException类及它的子类都是运行时异常。比如:ArrayIndexOutOfBoundsException数组下标越界异常,ClassCastException类型转换异常。

1562771528807

3. 常见的错误和异常

3.1 Error

最常见的就是VirtualMachineError,它有两个经典的子类:StackOverflowError、OutOfMemoryError。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.atguigu.exception;

import org.junit.Test;

public class TestStackOverflowError {
@Test
public void test01(){
//StackOverflowError
recursion();
}

public void recursion(){ //递归方法
recursion();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.atguigu.exception;

import org.junit.Test;

public class TestOutOfMemoryError {
@Test
public void test02(){
//OutOfMemoryError
//方式一:
int[] arr = new int[Integer.MAX_VALUE];
}
@Test
public void test03(){
//OutOfMemoryError
//方式二:
StringBuilder s = new StringBuilder();
while(true){
s.append("atguigu");
}
}
}

3.2 运行时异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.atguigu.exception;

import org.junit.Test;

import java.util.Scanner;

public class TestRuntimeException {
@Test
public void test01(){
//NullPointerException
int[][] arr = new int[3][];
System.out.println(arr[0].length);
}

@Test
public void test02(){
//ClassCastException
Object obj = 15;
String str = (String) obj;
}

@Test
public void test03(){
//ArrayIndexOutOfBoundsException
int[] arr = new int[5];
for (int i = 1; i <= 5; i++) {
System.out.println(arr[i]);
}
}

@Test
public void test04(){
//InputMismatchException
Scanner input = new Scanner(System.in);
System.out.print("请输入一个整数:");//输入非整数
int num = input.nextInt();
input.close();
}

@Test
public void test05(){
int a = 1;
int b = 0;
//ArithmeticException
System.out.println(a/b);
}
}

3.3 编译时异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.atguigu.exception;

import org.junit.Test;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class TestCheckedException {
@Test
public void test06() {
Thread.sleep(1000);//休眠1秒 InterruptedException
}

@Test
public void test07(){
Class c = Class.forName("java.lang.String");//ClassNotFoundException
}

@Test
public void test08() {
Connection conn = DriverManager.getConnection("...."); //SQLException
}
@Test
public void test09() {
FileInputStream fis = new FileInputStream("尚硅谷Java秘籍.txt"); //FileNotFoundException
}
@Test
public void test10() {
File file = new File("尚硅谷Java秘籍.txt");
FileInputStream fis = new FileInputStream(file);//FileNotFoundException
int b = fis.read();//IOException
while(b != -1){
System.out.print((char)b);
b = fis.read();//IOException
}

fis.close();//IOException
}
}

4. 异常的处理

4.1 异常处理概述

在编写程序时,经常要在可能出现错误的地方加上检测的代码,如进行x/y运算时,要检测分母为0数据为空输入的不是数据而是字符等。过多的if-else分支会导致程序的代码加长臃肿可读性差,程序员需要花很大的精力“堵漏洞”。因此采用异常处理机制。

Java异常处理

Java采用的异常处理机制,是将异常处理的程序代码集中在一起,与正常的程序代码分开,使得程序简洁、优雅,并易于维护。

Java异常处理的方式:

方式一:try-catch-finally

方式二:throws + 异常类型

4.2 方式1:捕获异常(try-catch-finally)

Java提供了异常处理的抓抛模型

  • 前面提到,Java程序的执行过程中如出现异常,会生成一个异常类对象,该异常对象将被提交给Java运行时系统,这个过程称为抛出(throw)异常
  • 如果一个方法内抛出异常,该异常对象会被抛给调用者方法中处理。如果异常没有在调用者方法中处理,它继续被抛给这个调用方法的上层方法。这个过程将一直继续下去,直到异常被处理。这一过程称为捕获(catch)异常
  • 如果一个异常回到main()方法,并且main()也不处理,则程序运行终止。

4.2.1 try-catch-finally基本格式

捕获异常语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
try{
...... //可能产生异常的代码
}
catch( 异常类型1 e ){
...... //当产生异常类型1型异常时的处置措施
}
catch( 异常类型2 e ){
...... //当产生异常类型2型异常时的处置措施
}
finally{
...... //无论是否发生异常,都无条件执行的语句
}

1、整体执行过程:

当某段代码可能发生异常,不管这个异常是编译时异常(受检异常)还是运行时异常(非受检异常),我们都可以使用try块将它括起来,并在try块下面编写catch分支尝试捕获对应的异常对象。

  • 如果在程序运行时,try块中的代码没有发生异常,那么catch所有的分支都不执行。
  • 如果在程序运行时,try块中的代码发生了异常,根据异常对象的类型,将从上到下选择第一个匹配的catch分支执行。此时try中发生异常的语句下面的代码将不执行,而整个try…catch之后的代码可以继续运行。
  • 如果在程序运行时,try块中的代码发生了异常,但是所有catch分支都无法匹配(捕获)这个异常,那么JVM将会终止当前方法的执行,并把异常对象“抛”给调用者。如果调用者不处理,程序就挂了。
image-20220503122722605

2、try

  • 捕获异常的第一步是用try{…}语句块选定捕获异常的范围,将可能出现异常的业务逻辑代码放在try语句块中。

3、catch (Exceptiontype e)

  • catch分支,分为两个部分,catch()中编写异常类型和异常参数名,{}中编写如果发生了这个异常,要做什么处理的代码。

  • 如果明确知道产生的是何种异常,可以用该异常类作为catch的参数;也可以用其父类作为catch的参数。

    比如:可以用ArithmeticException类作为参数的地方,就可以用RuntimeException类作为参数,或者用所有异常的父类Exception类作为参数。但不能是与ArithmeticException类无关的异常,如NullPointerException(catch中的语句将不会执行)。

  • 每个try语句块可以伴随一个或多个catch语句,用于处理可能产生的不同类型的异常对象。

  • 如果有多个catch分支,并且多个异常类型有父子类关系,必须保证小的子异常类型在上,大的父异常类型在下。否则,报错。

  • catch中常用异常处理的方式

    • public String getMessage():获取异常的描述信息,返回字符串

    • public void printStackTrace():打印异常的跟踪栈信息并输出到控制台。包含了异常的类型、异常的原因、还包括异常出现的位置,在开发和调试阶段,都得使用printStackTrace()。

    image-20220331180736381

4.2.2 使用举例

举例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class IndexOutExp {
public static void main(String[] args) {
String friends[] = {"lisa", "bily", "kessy"};
try {
for (int i = 0; i < 5; i++) { // i只能从0-2
System.out.println(friends[i]); // 可能出现数组下标
}
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("index err"); //前面三个正常输出 第四个就会被异常catch捕捉
}
System.out.println("\nthis is the end"); //记得前面有一个换行符
}
}
最终输出:
lisa
bily
kessy
index err

this is the end

举例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class DivideZero1 {
int x; //全局变量x
public static void main(String[] args) {
int y; //局部变量y
DivideZero1 c = new DivideZero1(); //创建类的对象
System.out.println("当前对象的属性x值为:"+c.x);
try {
y = 3 / c.x; //会出现下标异常
} catch (ArithmeticException e) {
System.out.println("divide by zero error!");
}
System.out.println("program ends ok!");
}
}
最终输出:
当前对象的属性x值为:0
divide by zero error!
program ends ok!

举例3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Test
public void test1(){
try{
String str1 = "atguigu.com";
str1 = null;
System.out.println(str1.charAt(0));
}catch(NullPointerException e){
//异常的处理方式1
System.out.println("不好意思,亲~出现了小问题,正在加紧解决...");
}catch(ClassCastException e){
//异常的处理方式2
System.out.println("出现了类型转换的异常");
}catch(RuntimeException e){
//异常的处理方式3
System.out.println("出现了运行时异常");
}
//此处的代码,在异常被处理了以后,是可以正常执行的
System.out.println("hello");
}
最终输出:
不好意思,亲~出现了小问题,正在加紧解决...
hello

4.2.3 finally使用及举例

image-20220331215517077
  • 因为异常会引发程序跳转,从而会导致有些语句执行不到。而程序中有一些特定的代码无论异常是否发生,都需要执行。例如,数据库连接、输入流输出流、Socket连接、Lock锁的关闭等,这样的代码通常就会放到finally块中。所以,我们通常将一定要被执行的代码声明在finally中。

    • 唯一的例外,使用 System.exit(0) 来终止当前正在运行的 Java 虚拟机。
  • 不论在try代码块中是否发生了异常事件,catch语句是否执行,catch语句是否有异常,catch语句中是否有return,finally块中的语句都会被执行。

  • finally语句和catch语句是可选的,但finally不能单独使用。

    1
    2
    3
    4
    5
    try{

    }finally{

    }

举例1:确保资源关闭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.util.InputMismatchException;
import java.util.Scanner;
public class TestFinally {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
try {
System.out.print("请输入第一个整数:");
int a = input.nextInt();
System.out.print("请输入第二个整数:");
int b = input.nextInt();
int result = a/b;
System.out.println(a + "/" + b +"=" + result);
} catch (InputMismatchException e) {
System.out.println("数字格式不正确,请输入两个整数");
}catch (ArithmeticException e){
System.out.println("第二个整数不能为0");
} finally {
System.out.println("程序结束,释放资源"); //一定会输出
input.close(); //输入流的关闭语句一定要写在finally语句
}
}
}
最终输出:
请输入第一个整数:2
请输入第二个整数:0
第二个整数不能为0
程序结束,释放资源

举例2:从try回来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class FinallyTest1 {
public static void main(String[] args) {
int result = test("12");
System.out.println(result);
}

public static int test(String str){
try{
Integer.parseInt(str); //字符串转为int包装类
return 1;
}catch(NumberFormatException e){
return -1;
}finally{
System.out.println("test结束"); //一定会执行
}
}
}
最后输出:
test结束
1 //这个1是try里面返回的

举例3:从catch回来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class FinallyTest2 {
public static void main(String[] args) {
int result = test("a");
System.out.println(result);
}

public static int test(String str) {
try {
Integer.parseInt(str); //字符串转为int包装类
return 1;
} catch (NumberFormatException e) { //有异常进入catch
return -1;
} finally {
System.out.println("test结束"); //一定会执行
}
}
}
最终输出:
test结束
-1 //这个-1是catch里面返回的

举例4:从finally回来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class FinallyTest3 {
public static void main(String[] args) {
int result = test("a");
System.out.println(result);
}

public static int test(String str) {
try {
Integer.parseInt(str);
return 1;
} catch (NumberFormatException e) {
return -1;
} finally {
System.out.println("test结束"); //一定会执行
return 0; //直接从finally输出0
}
}
}
最终输出:
test结束
0

笔试题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ExceptionTest {
public static void main(String[] args) {
int result = test();
System.out.println(result); //102
}

public static int test(){
int i = 100;
try {
return i; //i只能在try里面有效
} finally {
i++; //没有修改try里面的局部变量i的值
return ++i; //这时候返回的是102
}
}
}
最终输出:
102

笔试题:final、finally、finalize有什么区别?

4.2.4 练习

编写一个类ExceptionTest,在main方法中使用try、catch、finally,要求:

  • 在try块中,编写被零除的代码。

  • 在catch块中,捕获被零除所产生的异常,并且打印异常信息

  • 在finally块中,打印一条语句。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class ExceptionTest {
    public static void main(String[] args) {
    try {
    int a=1;
    int b=0;
    System.out.println(a/b);
    }
    catch (ArithmeticException e){
    System.out.println("不能被整除0");
    System.out.println(e.getMessage());
    e.printStackTrace();
    }
    finally {
    System.out.println("finally语句");
    }
    }
    }

    image-20231016141559508

4.2.5 异常处理的体会

  • 前面使用的异常都是RuntimeException类/它的子类,这些类异常的特点是:即使没有使用try和catch捕获,Java自己也能捕获,并且编译通过 ( 但运行时会发生异常使得程序运行终止 )。所以,对于这类异常,可以不作处理,因为这类异常很普遍,若全处理可能会对程序的可读性和运行效率产生影响。

  • 如果抛出的异常是IOException等类型的非运行时异常,则必须捕获,否则编译错误。也就是说,我们必须处理编译时异常,将异常进行捕捉,转化为运行时异常。

4.3 方式2:声明抛出异常类型(throws)

  • 如果在编写方法体的代码时,某句代码可能发生某个编译时异常,不处理编译不通过,但是在当前方法体中可能不适合处理无法给出合理的处理方式,则此方法应显示地声明抛出异常,表明该方法将不对这些异常进行处理,而由该方法的调用者负责处理。

    image-20220331112000671
  • 具体方式:在方法声明中用throws语句可以声明抛出异常的列表,throws后面的异常类型可以是方法中产生的异常类型,也可以是它的父类。

4.3.1 throws基本格式

声明异常格式:

1
修饰符 返回值类型 方法名(参数) throws 异常类名1,异常类名2…{   }

在throws后面可以写多个异常类型,用逗号隔开。

举例:

1
2
3
4
5
6
public void readFile(String file)  throws FileNotFoundException,IOException {
...
// 读文件的操作可能产生FileNotFoundException或IOException类型的异常
FileInputStream fis = new FileInputStream(file);
//...
}

4.3.2 throws 使用举例

举例:针对于编译时异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TestThrowsCheckedException {
public static void main(String[] args) {
System.out.println("上课.....");
try {
afterClass();//换到这里处理异常
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("准备提前上课");
}
System.out.println("上课.....");
}

public static void afterClass() throws InterruptedException {
for(int i=10; i>=1; i--){
Thread.sleep(1000);//本来应该在这里处理异常
System.out.println("距离上课还有:" + i + "分钟");
}
}
}

举例:针对于运行时异常:

throws后面也可以写运行时异常类型,只是运行时异常类型,写或不写对于编译器和程序执行来说都没有任何区别。如果写了,唯一的区别就是调用者调用该方法后,使用try…catch结构时,IDEA可以获得更多的信息,需要添加哪种catch分支。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.InputMismatchException;
import java.util.Scanner;
public class TestThrowsRuntimeException {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
try {
System.out.print("请输入第一个整数:");
int a = input.nextInt();
System.out.print("请输入第二个整数:");
int b = input.nextInt();
int result = divide(a,b);
System.out.println(a + "/" + b +"=" + result);
} catch (ArithmeticException | InputMismatchException e) {
e.printStackTrace();
} finally {
input.close();
}
}

public static int divide(int a, int b)throws ArithmeticException{
return a/b;
}
}

4.3.3 方法重写中throws的要求

方法重写时,对于方法签名是有严格要求的。复习:

1
2
3
4
5
6
7
(1)方法名必须相同
(2)形参列表必须相同
(3)返回值类型
- 基本数据类型和void:必须相同
- 引用数据类型:<=
(4)权限修饰符:>=,而且要求父类被重写方法在子类中是可见的
(5)不能是static,final修饰的方法

此外,对于throws异常列表要求:

1.编译时异常:

  • 如果父类被重写方法的方法签名后面没有 “throws 编译时异常类型”,那么重写方法时,方法签名后面也不能出现“throws 编译时异常类型”。

  • 如果父类被重写方法的方法签名后面有 “throws 编译时异常类型”,那么重写方法时,throws的编译时异常类型必须 <= 被重写方法throws的编译时异常类型,或者不throws编译时异常。

    2.运行时异常:

  • 方法重写,对于“throws 运行时异常类型”没有要求。

1
2
3
4
5
6
7
8
9
10
11
12
import java.io.IOException;
class Father{
public void method()throws Exception{
System.out.println("Father.method");
}
}
class Son extends Father{
@Override
public void method() throws IOException,ClassCastException {
System.out.println("Son.method");
}
}

4.4 两种异常处理方式的选择

前提:对于异常,使用相应的处理方式。此时的异常,主要指的是编译时异常。

  • 如果程序代码中,涉及到资源的调用(流、数据库连接、网络连接等),则必须考虑使用try-catch-finally来处理,保证不出现内存泄漏。
  • 如果父类被重写的方法没有throws异常类型,则子类重写的方法中如果出现异常,只能考虑使用try-catch-finally进行处理,不能throws。
  • 开发中,方法a中依次调用了方法b,c,d等方法,方法b,c,d之间是递进关系。此时,如果方法b,c,d中有异常,我们通常选择使用throws,而方法a中通常选择使用try-catch-finally。

5. 手动抛出异常对象:throw

Java 中异常对象的生成有两种方式:

  • 由虚拟机自动生成:程序运行过程中,虚拟机检测到程序发生了问题,那么针对当前代码,就会在后台自动创建一个对应异常类的实例对象并抛出。

  • 由开发人员手动创建new 异常类型([实参列表]);,如果创建好的异常对象不抛出对程序没有任何影响,和创建一个普通对象一样,但是一旦throw抛出,就会对程序运行产生影响了。

5.1 使用格式

1
throw new 异常类名(参数);

throw语句抛出的异常对象,和JVM自动创建和抛出的异常对象一样。

  • 如果是编译时异常类型的对象,需要使用throws或者try…catch处理,否则编译不通过。

  • 如果是运行时异常类型的对象,编译器不提示。

  • 可以抛出的异常必须是Throwable或其子类的实例。下面的语句在编译时将会产生语法错误:

    1
    throw new String("want to throw");

5.2 使用注意点:

无论是编译时异常类型的对象,还是运行时异常类型的对象,如果没有被try..catch合理的处理,都会导致程序崩溃。

throw语句会导致程序执行流程被改变,throw语句是明确抛出一个异常对象,因此它下面的代码将不会执行

如果当前方法没有try…catch处理这个异常对象,throw语句就会代替return语句提前终止当前方法的执行,并返回一个异常对象给调用者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class TestThrow {
public static void main(String[] args) {
//第一个try-catch
try {
System.out.println(max(4,2,31,1));
} catch (Exception e) {
e.printStackTrace();
}
//第二个try-catch
try {
System.out.println(max(4));
} catch (Exception e) {
e.printStackTrace();
}
//第三个try-catch
try {
System.out.println(max()); //有问题
} catch (Exception e) {
e.printStackTrace();
}
}
//寻找数组中的最大值
public static int max(int... nums){
if(nums == null || nums.length==0){ //nums为空
throw new IllegalArgumentException("没有传入任何整数,无法获取最大值"); //抛出一个异常
}
int max = nums[0];
for (int i = 1; i < nums.length; i++) {
if(nums[i] > max){
max = nums[i];
}
}
return max;
}
}
最终输出:
31
4
java.lang.IllegalArgumentException: 没有传入任何整数,无法获取最大值
at ExceptionTest.TestThrow.max(TestThrow.java:27)
at ExceptionTest.TestThrow.main(TestThrow.java:19)

6. 自定义异常类

6.1 为什么需要自定义异常类

Java中不同的异常类,分别表示着某一种具体的异常情况。那么在开发中总是有些异常情况是核心类库中没有定义好的,此时我们需要根据自己业务的异常情况来定义异常类。例如年龄负数问题,考试成绩负数问题,某员工已在团队中等。

6.2 如何自定义异常类

(1)要继承一个异常类型

​ 自定义一个编译时异常类型:自定义类继承java.lang.Exception

​ 自定义一个运行时异常类型:自定义类继承java.lang.RuntimeException

(2)建议大家提供至少两个构造器,一个是无参构造,一个是(String message)构造器。

(3)自定义异常需要提供一个全局常量,—> static final long serialVersionUID

例如:

6.3 注意点

  1. 自定义的异常只能通过throw抛出

  2. 自定义异常最重要的是异常类的名字和message属性。当异常出现时,可以根据名字判断异常类型。比如:TeamException("成员已满,无法添加");TeamException("该员工已是某团队成员");

  3. 自定义异常对象只能手动抛出。抛出后由try..catch处理,也可以甩锅throws给调用者处理。

6.4 举例

举例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//自定义异常:
class MyException extends Exception { //继承编译时异常类型
//1个全局常量
static final long serialVersionUID = 23423423435L;
private int idnumber;
//构造器
public MyException(String message, int id) {
super(message);
this.idnumber = id;
}

public int getId() {
return idnumber;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class MyExpTest {
public static void main(String args[]) {
MyExpTest t = new MyExpTest();
t.manager();
}

public void manager(){
try{
regist(-100); //对regist方法进行检测
}catch (MyException e){ //使用自己定义的MyException类
System.out.println("登记失败,出错种类:" + e.getId());
System.out.println("输出信息:"+e.getMessage());
System.out.println("输出报错的原因:"+e.getCause());
}finally {
System.out.print("本次登记操作结束!"); //一定会执行
}
}

public void regist(int num) throws MyException{ //捕获异常 返回给被调用的manager方法【在它方法内使用try-catch】
if(num<0){
throw new MyException("人数为负数,不合理",3); //抛出异常
}else{
System.out.println("登记人数"+num);
}
}

}

image-20231016170808072

举例2:

1
2
3
4
5
6
7
8
9
10
11
12
//自定义异常:
public class NotTriangleException extends Exception{ //继承编译时异常类型
//一个全局常量
static final long serialVersionUID = 13465653435L;
//无参构造器
public NotTriangleException() {
}
//有参构造器
public NotTriangleException(String message) {
super(message);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public class Triangle {
private double a;
private double b;
private double c;

public Triangle(double a, double b, double c) throws NotTriangleException {
if(a<=0 || b<=0 || c<=0){
throw new NotTriangleException("三角形的边长必须是正数"); //抛出自定义异常类对象
}
if(a+b<=c || b+c<=a || a+c<=b){
throw new NotTriangleException(a+"," + b +"," + c +"不能构造三角形,三角形任意两边之后必须大于第三边"); //抛出自定义异常类对象
}
this.a = a;
this.b = b;
this.c = c;
}

public double getA() {
return a;
}

public void setA(double a) throws NotTriangleException{
if(a<=0){
throw new NotTriangleException("三角形的边长必须是正数"); //抛出自定义异常类对象
}
if(a+b<=c || b+c<=a || a+c<=b){
throw new NotTriangleException(a+"," + b +"," + c +"不能构造三角形,三角形任意两边之后必须大于第三边"); //抛出自定义异常类对象
}
this.a = a;
}

public double getB() {
return b;
}

public void setB(double b) throws NotTriangleException {
if(b<=0){
throw new NotTriangleException("三角形的边长必须是正数"); //抛出自定义异常类对象
}
if(a+b<=c || b+c<=a || a+c<=b){
throw new NotTriangleException(a+"," + b +"," + c +"不能构造三角形,三角形任意两边之后必须大于第三边"); //抛出自定义异常类对象
}
this.b = b;
}

public double getC() {
return c;
}

public void setC(double c) throws NotTriangleException {
if(c<=0){
throw new NotTriangleException("三角形的边长必须是正数"); //抛出自定义异常类对象
}
if(a+b<=c || b+c<=a || a+c<=b){
throw new NotTriangleException(a+"," + b +"," + c +"不能构造三角形,三角形任意两边之后必须大于第三边"); //抛出自定义异常类对象
}
this.c = c;
}

@Override
public String toString() {
return "Triangle{" +"a=" + a +", b=" + b +", c=" + c +'}';
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class TestTriangle {
public static void main(String[] args) {
Triangle t = null;
//第一个try 测试构造器
try {
t = new Triangle(2,2,3);
System.out.println("三角形创建成功:");
System.out.println(t);
} catch (NotTriangleException e) { //出现在自定义异常类
System.err.println("三角形创建失败");
e.printStackTrace();
}
//第二个try 测试setA方法
try {
if(t != null) {
t.setA(1);
}
System.out.println("三角形边长修改成功");
} catch (NotTriangleException e) { //出现在自定义异常类
System.out.println("三角形边长修改失败");
e.printStackTrace();
}
}
}

image-20231016171600914

7. 练习

练习1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class ReturnExceptionDemo {
//方法 methodA
static void methodA() {
try {
System.out.println("进入方法A");
throw new RuntimeException("制造异常"); //抛出一个运行时异常
}finally {
System.out.println("用A方法的finally"); //一定会执行
}
}
//方法 methodB
static void methodB() {
try {
System.out.println("进入方法B");
return;
} finally {
System.out.println("调用B方法的finally"); //一定会执行
}
}
//代码执行main方法
public static void main(String[] args) {
//执行顺序:
//进入try块 执行methodA方法
//进入methodA方法的try块 输出进入方法A 然后执行finally语句 输出用A方法的finally 之后将抛出的运行时异常给调用者main方法 进入catch就输出制造异常
//最后执行methodB方法
//进入方法B的try块 输出进入方法B 然后执行finally输出调用B方法的finally 最后不返回值
try {
methodA();
} catch (Exception e) {
System.out.println(e.getMessage());
}
//调用方法B
methodB();
}
}
最终输出:
进入方法A
用A方法的finally
制造异常
进入方法B
调用B方法的finally

练习2:

从键盘接收学生成绩,成绩必须在0~100之间。

自定义成绩无效异常。

编写方法接收成绩并返回该成绩,如果输入无效,则抛出自定义异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
//自定义异常类
public class GradeNotException extends Exception{
static final long serialVersionUID=-123412421L;
public GradeNotException(){

}
public GradeNotException(String name){
super(name);
}
public GradeNotException(String message,Throwable cause){
super(message,cause);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//手动Stduent类使用设定的自定义异常
import java.util.Scanner;
public class Student {
public static void main(String[] args) {
Scanner input=new Scanner(System.in);
try{
System.out.println("输入学生成绩");
int score=getValidScore(input); //调用方法
}catch(GradeNotException e){ //catch为自定义异常类
System.out.println(e.getMessage()); //输出异常信息 --就是throw new GradeNotException括号里面内容
}finally{
System.out.println("finally语句"); //一定执行
}
}

public static int getValidScore(Scanner input) throws GradeNotException{ //抛出自定义异常
System.out.println("输出成绩:");
int score=input.nextInt(); //从键盘输入成绩score
if(score<0||score>100){
throw new GradeNotException("输入成绩无效"); //只能throw手动抛出
}
return score;
}
}

代码结果如下:

image-20231017094258915

练习3:

编写应用程序EcmDef.java,接收命令行的两个参数,要求不能输入负数,计算两数相除。
对数据类型不一致(NumberFormatException)、缺少命令行参数(ArrayIndexOutOfBoundsException、
除0(ArithmeticException)及输入负数(EcDef 自定义的异常)进行异常处理。

提示:
(1)在主类(EcmDef)中定义异常方法(ecm)完成两数相除功能。

(2)在main()方法中使用异常处理语句进行异常处理。

(3)在程序中,自定义对应输入负数的异常类(EcDef)。

(4)运行时接受参数 java EcmDef 20 10 //args[0]=“20” args[1]=“10”

(5)Interger类的static方法parseInt(String s)将s转换成对应的int值。
如:int a=Interger.parseInt(“314”); //a=314;

1
2
3
4
5
6
7
8
9
10
//自定义异常类
public class EcmDefTest extends Exception{
static final long serialVersionUID=-123148L;
public EcmDefTest(){

}
public EcmDefTest(String name){
super(name);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class EcmDef {
public static void main(String[] args) {
//写try-catch-finally块
try{
if(args.length!=2){
System.out.println("没有获取两个命令行参数");
throw new ArrayIndexOutOfBoundsException("需要提供两个命令行参数");
}
//因为main里面是字符串数组//要对比数据类型不一致 那么就要将String转为包装类对比
int num1=Integer.parseInt(args[0]);
int num2=Integer.parseInt(args[1]);
//做除法
getChu(num1,num2);
}catch(NumberFormatException e){
System.out.println("发生的是NumberFormatException(数据类型不一致)异常类");
e.getMessage();
}catch(ArrayIndexOutOfBoundsException e){
System.out.println("发生的是ArrayIndexOutOfBoundsException(缺少命令行参数)异常类");
e.getMessage();
}catch(ArithmeticException e){
System.out.println("发生的是ArithmeticException(除0)异常类");
e.getMessage();
}catch(EcmDefTest e){
System.out.println("发生的是EcmDefTest(输入负数)自定义异常类");
e.getMessage();
}finally{
System.out.println("finally语句");
}

}
public static void getChu(int num1,int num2) throws EcmDefTest {
if(num1<0||num2<0){
throw new EcmDefTest("输入不能为负数");
}
if(num2==0){
throw new ArithmeticException("被除数为0");
}
int res=num1/num2;
System.out.println("res"+res);
}

}

image-20231017102148928

8. 小结与小悟

8.1 小结:异常处理5个关键字

image-20220330003738109

类比:上游排污,下游治污

8.2 感悟

小哲理:

世界上最遥远的距离,是我在if里你在else里,似乎一直相伴又永远分离;

世界上最痴心的等待,是我当case你是switch,或许永远都选不上自己;

世界上最真情的相依,是你在try我在catch。无论你发神马脾气,我都默默承受,静静处理。到那时,再来期待我们的finally

歌词:

image-20220511163035280

JAVA-面向对象

第06章_面向对象编程(基础)

学习面向对象内容的三条主线

• Java类及类的成员:(重点)属性、方法、构造器;(熟悉)代码块、内部类

• 面向对象的特征:封装、继承、多态、(抽象)

• 其他关键字的使用:this、super、package、import、static、final、interface、abstract等

1. 面向对象编程概述(了解)

1.1 程序设计的思路

面向对象,是软件开发中的一类编程风格、开发范式。除了面向对象,还有面向过程、指令式编程和函数式编程。在所有的编程范式中,我们接触最多的还是面向过程和面向对象两种。

类比:史书类型

• 纪传体:以人物传记为中心,“本纪”叙述帝王,“世家”记叙王侯封国和特殊人物,“列传”记叙民间人物。

• 编年体:按年、月、日顺序编写。

• 国别体:是一部分国记事的历史散文,分载多国历史。

早期先有面向过程思想,随着软件规模的扩大,问题复杂性的提高,面向过程的弊端越来越明显,出现了面向对象思想并成为目前主流的方式。

1. 面向过程的程序设计思想(Process-Oriented Programming),简称POP

关注的焦点是过程:过程就是操作数据的步骤。如果某个过程的实现代码重复出现,那么就可以把这个过程抽取为一个函数。这样就可以大大简化冗余代码,便于维护。

典型的语言:C语言

代码结构:以函数为组织单位。

是一种“执行者思维”,适合解决简单问题。扩展能力差、后期维护难度较大。

2. 面向对象的程序设计思想( Object Oriented Programming),简称OOP

关注的焦点是类:在计算机程序设计过程中,参照现实中事物,将事物的属性特征、行为特征抽象出来,用类来表示。

典型的语言:Java、C#、C++、Python、Ruby和PHP等

代码结构:以类为组织单位。每种事物都具备自己的属性和行为/功能。

是一种“设计者思维”,适合解决复杂问题。代码扩展性强、可维护性高。

1.2 由实际问题考虑如何设计程序

可随着需求的更改,功能的增多,发现需要面对每一个步骤很麻烦了,这时就开始思索,能不能把这些步骤和功能进行封装,封装时根据不同的功能,进行不同的封装,功能类似的封装在一起。这样结构就清晰了很多。用的时候,找到对应的类就可以了。这就是面向对象的思想。

类比举例2:人把大象装进冰箱

面向过程:

1
2
3
1.打开冰箱
2.把大象装进冰箱
3.把冰箱门关住

面向对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//分为三个类
人{
打开(冰箱){
冰箱.开门();
}
操作(大象){
大象.进入(冰箱);
}
关闭(冰箱){
冰箱.关门();
}
}

冰箱{
开门(){ }
关门(){ }
}

大象{
进入(冰箱){
}
}

2. Java语言的基本元素:类和对象

2.1 引入

人认识世界,其实就是面向对象的。比如,我们认识一下美人鱼(都没见过)

image-20220520162643547

经过“仔细学习”,发现美人鱼通常具备一些特征:

• 女孩

• 有鱼尾

• 美丽

这个总结的过程,其实是抽象化的过程。抽象出来的美人鱼的特征,可以归纳为一个美人鱼类。而图片中的都是这个类呈现出来的具体的对象。

2.2 类和对象概述

类(Class)和对象(Object)是面向对象的核心概念。

1**、什么是类**

:具有相同特征的事物的抽象描述,是抽象的、概念上的定义。

2**、什么是对象**

对象:实际存在的该类事物的每个个体,是具体的,因而也称为实例(instance)。

img

可以理解为:类 => 抽象概念的人;对象 => 实实在在的某个人

img

img

3**、类与对象的关系错误理解**

曰:“白马非马,可乎?”
曰:“可。”
曰:“何哉?”
曰:“马者,所以命形也。白者,所以命色也。命色者,非命形也,故曰白马非马。”

img

2.3 类的成员概述

面向对象程序设计的重点是类的设计

Java中用类class来描述事物也是如此。类,是一组相关属性和行为的集合,这也是类最基本的两个成员。

属性:该类事物的状态信息。对应类中的成员变量

成员变量 <=> 属性 <=> Field

行为:该类事物要做什么操作,或者基于事物的状态能做什么。对应类中的成员方法

(成员)方法 <=> 函数 <=> Method

20220319_211611

举例:

img

2.4 面向对象完成功能的三步骤(重要)

步骤1:类的定义

1
2
3
4
5
6
类的定义使用关键字:class。格式如下:

[修饰符] class 类名{
属性声明;
方法声明;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
举例1:
public class Person{
*//**声明属性age*
int age ;

*//**声明方法showAge()*
public void eat() {
System.out.println("人吃饭");
}
}

举例2:

public class Dog{
*//**声明属性*
String type; *//**种类*
String nickName; *//**昵称*
String hostName; *//**主人名称*

*//**声明方法*
public void eat(){ *//**吃东西*
System.out.println("狗狗进食");
}
}

public class Person{
String name;
char gender;
Dog dog;

*//**喂宠物*
public void feed(){
dog.eat();
}
}

步骤2:对象的创建

image-20220319213201568

创建对象,使用关键字:new

创建对象语法:

1
2
3
4
5
6
方式1:给创建的对象命名*
把创建的对象用一个引用数据类型的变量保存起来,这样就可以反复使用这个对象了*
类名 对象名 = new 类名();

方式2:
new 类名()*//**也称为匿名对象

举例:

1
2
3
4
5
6
7
8
class PersonTest{
public static void main(String[] args){
*//**创建Person**类的对象*
Person per = new Person();
*//**创建Dog**类的对象*
Dog dog = new Dog();
}
}

步骤3:对象调用属性或方法

对象是类的一个实例,必然具备该类事物的属性和行为(即方法)。

使用”对象名.属性” 或 “对象名.方法”的方式访问对象成员(包括属性和方法)

举例1:

1
2
3
4
5
6
7
8
9
10
11
12
//**声明Animal**类*
public class Animal { *//**动物类*
public int legs;

public void eat() {
System.out.println("Eating.");
}

public void move() {
System.out.println("Move.");
}
}
1
2
3
4
5
6
7
8
9
10
11
//声明测试类
public class AnimalTest {
public static void main(String args[]) {
*//**创建对象*
Animal xb = new Animal();
xb.legs = 4;*//**访问属性*
System.out.println(xb.legs);
xb.eat();*//**访问方法*
xb.move();*//**访问方法*
}
}

图示理解:标题: fig:

举例2:针对前面步骤1的举例2:类的实例化(创建类的对象)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Game{
public static void main(String[] args){
Person p = new Person();
*//**通过Person**对象调用属性*
p.name = "康师傅";
p.gender = '男';
p.dog = new Dog(); *//**给Person**对象的dog**属性赋值*
*//**给Person**对象的dog**属性的type**、nickname**属性赋值*
p.dog.type = "柯基犬";
p.dog.nickName = "小白";

*//**通过Person**对象调用方法*
p.feed();
}
}

2.5 匿名对象 (anonymous object)

我们也可以不定义对象的句柄,而直接调用这个对象的方法。这样的对象叫做匿名对象。

– 如:new Person().shout();

使用情况

– 如果一个对象只需要进行一次方法调用,那么就可以使用匿名对象。

– 我们经常将匿名对象作为实参传递给一个方法调用。

3. 对象的内存解析

3.1 JVM内存结构划分

HotSpot Java虚拟机的架构图如下。其中我们主要关心的是运行时数据区部分(Runtime Data Area)。

img

其中:

堆(Heap):此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。这一点在Java虚拟机规范中的描述是:所有的对象实例以及数组都要在堆上分配。

栈(Stack):是指虚拟机栈。虚拟机栈用于存储局部变量等。局部变量表存放了编译期可知长度的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不等同于对象本身,是对象在堆内存的首地址)。 方法执行完,自动释放。

方法区(Method Area):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

3.2 对象内存解析

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person { *//**类:人*
String name;
int age;
boolean isMale;
}

public class PersonTest { *//**测试类*
public static void main(String[] args) {
Person p1 = new Person();
p1.name = "赵同学";
p1.age = 20;
p1.isMale = true;

Person p2 = new Person();
p2.age = 10;

Person p3 = p1;
p3.name = "郭同学";

}
}

内存解析图:

image-20220319215723183

说明:

堆:凡是new出来的结构(对象、数组)都放在堆空间中。

对象的属性存放在堆空间中。

创建一个类的多个对象(比如p1、p2),则每个对象都拥有当前类的一套”副本”(即属性)。当通过一个对象修改其属性时,不会影响其它对象此属性的值。

当声明一个新的变量使用现有的对象进行赋值时(比如p3 = p1),此时并没有在堆空间中创建新的对象。而是两个变量共同指向了堆空间中同一个对象。当通过一个对象修改属性时,会影响另外一个对象对此属性的调用。

面试题:对象名中存储的是什么呢?

答:对象地址

1
2
3
4
5
6
7
8
9
10
11
12
public class StudentTest{
public static void main(String[] args){
System.out.println(new Student());*//Student@7852e922*

Student stu = new Student();
System.out.println(stu);*//Student@4e25154f*

int[] arr = new int[5];
System.out.println(arr);*//[I@70dea4e*

}
}

直接打印对象名和数组名都是显示“类型@对象的hashCode值”,所以说类、数组都是引用数据类型,引用数据类型的变量中存储的是对象的地址,或者说指向堆中对象的首地址。

img

3.3 练习

根据代码,画出内存图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Car {
String color = "red";
int num = 4;

void show() {
System.out.println("color=" + color + ",num=" + num);
}
}

class CarTest {
public static void main(String[] args) {
Car c1 = new Car(); *//**建立对象c1*
Car c2 = new Car(); *//**建立对象c2*
c1.color = "blue"; *//**对对象的属性进行修改*
c1.show(); *//**使用对象的方法*
c2.show();
}
}

4. 类的成员之一:成员变量(field)

4.1 如何声明成员变量

语法格式:

1
2
3
[修饰符1] class 类名{
[修饰符2] 数据类型 成员变量名 [= 初始化值];
}

说明:

– 位置要求:必须在类中,方法外

– 修饰符2(暂不考虑)

• 常用的权限修饰符有:private、缺省、protected、public

• 其他修饰符:static、final

– 数据类型

• 任何基本数据类型(如int、Boolean) 或 任何引用数据类型。

– 成员变量名

• 属于标识符,符合命名规则和规范即可。

– 初始化值

• 根据情况,可以显式赋值;也可以不赋值,使用默认值

示例:

1
2
3
4
public class Person{
private int age; *//**声明private**变量 age*
public String name = “Lila”; *//**声明public**变量 name*
}

4.2 成员变量 vs 局部变量

1**、变量的分类:成员变量与局部变量**

在方法体外,类体内声明的变量称为成员变量。

在方法体内部等位置声明的变量称为局部变量。

image-20220511101608038

image-20220319230744617

其中,static可以将成员变量分为两大类,静态变量和非静态变量。其中静态变量又称为类变量,非静态变量又称为实例变量或者属性。接下来先学习实例变量。

2**、成员变量 与 局部变量 的对比**

相同点:

– 变量声明的格式相同: 数据类型 变量名 = 初始化值

– 变量必须先声明、后初始化、再使用。

– 变量都有其对应的作用域。只在其作用域内是有效的

不同点:

1、声明位置和方式 (1)实例变量:在类中方法外 (2)局部变量:在方法体{}中或方法的形参列表、代码块中

2、在内存中存储的位置不同 (1)实例变量:堆 (2)局部变量:栈

3、生命周期 (1)实例变量:和对象的生命周期一样,随着对象的创建而存在,随着对象被GC回收而消亡, 而且每一个对象的实例变量是独立的。 (2)局部变量:和方法调用的生命周期一样,每一次方法被调用而在存在,随着方法执行的结束而消亡, 而且每一次方法调用都是独立。

4、作用域 (1)实例变量:通过对象就可以使用,本类中直接调用,其他类中“对象.实例变量” (2)局部变量:出了作用域就不能使用

5、修饰符(后面来讲) (1)实例变量:public,protected,private,final,volatile,transient等 (2)局部变量:final

6、默认值 (1)实例变量:有默认值 (2)局部变量:没有,必须手动初始化。其中的形参比较特殊,靠实参给它初始化。

3**、对象属性的默认初始化赋值**

当一个对象被创建时,会对其中各种类型的成员变量自动进行初始化赋值。

image-20220319231821747

4、举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person {*//**人类*
*//1.**属性*
String name;*//**姓名*
int age = 1;*//**年龄*
boolean isMale;*//**是否是男性*

public void show(String nation) {
*//nation:**局部变量*
String color;*//color:**局部变量*
color = "yellow";
}
}

*//**测试类*
class PersonTest {
public static void main(String[] args) {
Person p = new Person();
p.show("CHN");
}
}

image-20220319231703240

1. 类的成员之二:方法(method)

5.1 方法的引入

img

《街霸》游戏中,每次人物出拳、出脚或跳跃等动作都需要编写50-80行的代码,在每次出拳、出脚或跳跃的地方都需要重复地编写这50-80行代码,这样程序会变得很臃肿,可读性也非常差。为了解决代码重复编写的问题,可以将出拳、出脚或跳跃的代码提取出来放在一个{}中,并为这段代码起个名字,这样在每次的出拳、出脚或跳跃的地方通过这个名字来调用这个{}的代码就可以了。

上述过程中,所提取出来的代码可以被看作是程序中定义的一个方法,程序在需要出拳、出脚或跳跃时调用该方法即可。

5.2 方法(method、函数)的理解

方法是类或对象行为特征的抽象,用来完成某个功能操作。在某些语言中也称为函数或过程。

将功能封装为方法的目的是,可以实现代码重用,减少冗余,简化代码

Java里的方法不能独立存在,所有的方法必须定义在类里。

举例1:

1
2
3
4
5
6
7
8
9
–     Math.random()的random()方法

– Math.sqrt(x)的sqrt(x)方法

– System.out.println(x)的println(x)方法

– new Scanner(System.in).nextInt()的nextInt()方法

– Arrays类中的binarySearch()方法、sort()方法、equals()方法

举例2:

1
2
3
4
5
6
7
8
9
  public class Person{
private int age;
public int getAge() { *//**声明方法getAge()*
return age;
}
public void setAge(int i) { *//**声明方法setAge*
age = i; *//**将参数i**的值赋给类的成员变量age*
}
}

5.3 如何声明方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
1****、声明方法的语法格式**

[修饰符] 返回值类型 方法名([形参列表])[throws 异常列表]{
方法体的功能代码
}

(1)一个完整的方法 = 方法头 + 方法体。

方法头就是[修饰符] 返回值类型 方法名([形参列表])[throws 异常列表],也称为方法签名。通常调用方法时只需要关注方法头就可以,从方法头可以看出这个方法的功能和调用格式。

方法体就是方法被调用后要执行的代码。对于调用者来说,不了解方法体如何实现的,并不影响方法的使用。

**(2)方法头可能包含5个部分**

**修饰符**:可选的。方法的修饰符也有很多,例如:public、protected、private、static、abstract、native、final、synchronized等,后面会一一学习。

– 其中,权限修饰符有public、protected、private。在讲封装性之前,我们先默认使用pulbic修饰方法。

– 其中,根据是否有static,可以将方法分为静态方法和非静态方法。其中静态方法又称为类方法,非静态方法又称为实例方法。咱们在讲static前先学习实例方法。

**返回值类型**: 表示方法运行的结果的数据类型,方法执行后将结果返回到调用者。

– 无返回值,则声明:void

– 有返回值,则声明出返回值类型(可以是任意类型)。与方法体中“return 返回值”搭配使用

**方法名**:属于标识符,命名时遵循标识符命名规则和规范,“见名知意”

**形参列表**:表示完成方法体功能时需要外部提供的数据列表。可以包含零个,一个或多个参数。

– 无论是否有参数,()不能省略

– 如果有参数,每一个参数都要指定数据类型和参数名,多个参数之间使用逗号分隔,例如:

• 一个参数: (数据类型 参数名)

• 二个参数: (数据类型1 参数1, 数据类型2 参数2)

– 参数的类型可以是基本数据类型、引用数据类型

**throws 异常列表**:可选,在【第09章-异常处理】章节再讲

**(3)方法体**:方法体必须有{}括起来,在{}中编写完成方法功能的代码

**(4)关于方法体中return语句的说明:**

return语句的作用是结束方法的执行,并将方法的结果返回去

如果返回值类型不是void,方法体中必须保证一定有 return 返回值; 语句,并且要求该返回值结果的类型与声明的返回值类型一致或兼容。

如果返回值类型为void时,方法体中可以没有return语句,如果要用return语句提前结束方法的执行,那么return后面不能跟返回值,直接写return ; 就可以。

return语句后面就不能再写其他代码了,否则会报错:Unreachable code

补充:方法的分类:按照是否有形参及返回值

image-20220320000047155

2、类比举例

image-20220503102323689

3、代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.atguigu.test04.method;

*/***
*** *方法定义案例演示*
**/*
public class MethodDefineDemo {
*/***
*** *无参无返回值方法的演示*
**/*
public void sayHello(){
System.out.println("hello");
}

*/***
*** *有参无返回值方法的演示*
** @*param length int *第一个参数,表示矩形的长*
*** *@*param width int *第二个参数,表示矩形的宽*
*** *@*param sign char *第三个参数,表示填充矩形图形的符号*
**/*
public void printRectangle(int length, int width, char sign){
for (int i = 1; i <= length ; i++) {
for(int j=1; j <= width; j++){
System.out.print(sign);
}
System.out.println();
}
}

*/***
*** *无参有返回值方法的演示*
** @*return
**/*
public int getIntBetweenOneToHundred(){
return (int)(Math.random()*100+1);
}

*/***
*** *有参有返回值方法的演示*
** @*param a int *第一个参数,要比较大小的整数之一*
*** *@*param b int *第二个参数,要比较大小的整数之二*
*** *@*return int *比较大小的两个整数中较大者的值*
**/*
public int max(int a, int b){
return a > b ? a : b;
}
}

5.4 如何调用实例方法

方法通过方法名被调用,且只有被调用才会执行。

1、方法调用语法格式

1
对象.方法名([实参列表])

2、示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
举例1:

package com.atguigu.test04.method;

*/***
*** *方法调用案例演示*
**/*
public class MethodInvokeDemo {
public static void main(String[] args) {
*//**创建对象*
MethodDefineDemo md = new MethodDefineDemo();

System.out.println("-----------------------方法调用演示-------------------------");

*//**调用MethodDefineDemo**类中无参无返回值的方法sayHello*
md.sayHello();
md.sayHello();
md.sayHello();
*//**调用一次,执行一次,不调用不执行*

System.out.println("------------------------------------------------");
*//**调用MethodDefineDemo**类中有参无返回值的方法printRectangle*
md.printRectangle(5,10,'@');

System.out.println("------------------------------------------------");
*//**调用MethodDefineDemo**类中无参有返回值的方法getIntBetweenOneToHundred*
md.getIntBetweenOneToHundred();*//**语法没问题,就是结果丢失*

int num = md.getIntBetweenOneToHundred();
System.out.println("num = " + num);

System.out.println(md.getIntBetweenOneToHundred());
*//**上面的代码调用了getIntBetweenOneToHundred**三次,这个方法执行了三次*

System.out.println("------------------------------------------------");
*//**调用MethodDefineDemo**类中有参有返回值的方法max*
md.max(3,6);*//**语法没问题,就是结果丢失*

int bigger = md.max(5,6);
System.out.println("bigger = " + bigger);

System.out.println("8,3中较大者是:" + md.max(8,9));

}
}
1
2
3
4
5
6
7
8
9
10
举例2:

*//1**、创建Scanner**的对象*
Scanner input = new Scanner(System.in);*//System.in**默认代表键盘输入*

*//2**、提示输入xx*
System.out.print("请输入一个整数:"); *//**对象.**非静态方法(**实参列表)*

*//3**、接收输入内容*
int num = input.nextInt(); *//**对象.**非静态方法()

5.5 使用的注意点

(1)必须先声明后使用,且方法必须定义在类的内部

(2)调用一次就执行一次,不调用不执行。

(3)方法中可以调用类中的方法或属性,不可以在方法内部定义方法。

正确示例:

1
2
3
4
5
6
7
8
类{
方法1(){

}
方法2(){

}
}

错误示例:

1
2
3
4
5
6
7
8
9
类{
方法1(){
方法2(){ *//**位置错误*


}

}
}

5.6 关键字return的使用

return在方法中的作用:

– 作用1:结束一个方法

– 作用2:结束一个方法的同时,可以返回数据给方法的调用者

注意点:在return关键字的直接后面不能声明执行语句

5.7 方法调用内存分析

方法没有被调用的时候,都在方法区中的字节码文件(.class)中存储。

方法被调用的时候,需要进入到栈内存中运行。方法每调用一次就会在栈中有一个入栈动作,即给当前方法开辟一块独立的内存区域,用于存储当前方法的局部变量的值。

当方法执行结束后,会释放该内存,称为出栈,如果方法有返回值,就会把结果返回调用处,如果没有返回值,就直接结束,回到调用处继续执行下一条指令。

栈结构:先进后出,后进先出。

举例分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Person {
public static void main(String[] args) {
Person p1 = new Person();
p1.eat();

}
public static void eat() {
sleep();
System.out.println("人:吃饭");
}
public static void sleep(){
System.out.println("人:睡觉");
doSport();
}
public static void doSport(){
System.out.println("人:运动");
}
}

内存分析:

img

5.8 练习

练习1:创建一个Person类,其定义如下:

image-20220320001337145

要求:

(1)创建Person类的对象,设置该对象的name、age和sex属性,调用study方法,输出字符串“studying”,调用showAge()方法显示age值,调用addAge()方法给对象的age属性值增加2岁。 (2)创建第二个对象,执行上述操作,体会同一个类的不同对象之间的关系。

练习2:利用面向对象的编程方法,设计圆类Circle,包含属性(半径)和计算圆面积的方法。定义测试类,创建该Circle类的对象,并进行测试。

练习3:

3.1 编写程序,声明一个method方法,在方法中打印一个108的型矩形,在main方法中调用该方法。

3.2 修改上一个程序,在method方法中,除打印一个108的型矩形外,再计算该矩形的面积,并将其作为方法返回值。在main方法中调用该方法,接收返回的面积值并打印。

3.3 修改上一个程序,在method方法提供m和n两个参数,方法中打印一个mn的型矩形,并计算该矩形的面积, 将其作为方法返回值。在main方法中调用该方法,接收返回的面积值并打印。

练习4:声明一个日期类型MyDate:有属性:年year,月month,日day。创建2个日期对象,分别赋值为:你的出生日期,你对象的出生日期,并显示信息。

练习5(课下练习):用面向对象的方式编写用户登录程序。

用户类:

属性:用户名,密码

方法:登录

界面类:

在界面类中添加main方法,接受用户输入,并调用用户类的登录方法进行验证。

– 输出:

• 登录失败:用户名或密码错误!

• 登录成功:欢迎你,用户名!

参考代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public class User {
String name;
String password;*//**密码*

*/***
*** *实现用户登录的判断*

***

** @*param inputName *输入的用户名*
*** *@*param inputPwd *输入的密码*
**/*
public void login(String inputName,String inputPwd){
if(name.equals(inputName) && password.equals(inputPwd)){
System.out.println("登录成功:欢迎你," + name);
}else{
System.out.println("登录失败:用户名或密码错误!");
}
}

*/***
*** *实现用户登录的判断*
** @*param inputName *输入的用户名*
*** *@*param inputPwd *输入的密码*
*** *@*return true*:**登录成功* false*:**登录失败*
**/*
public boolean login1(String inputName,String inputPwd){
*// if(name.equals(inputName) && password.equals(inputPwd)){*
*// return true;*
*// }else{*
*// return false;*
*// }*

*//**简化为:*
return name.equals(inputName) && password.equals(inputPwd);

}

}

*** *用户界面类*UserInterface*:*

***

*** *-* *在用户界面类中添加*main*方法,接受用户输入,并调用用户类的登录方法进行验证。*
*** *-* *输出:*
*** *-* *登录失败:用户名或密码错误!*
*** *-* *登录成功:欢迎你,用户名!*
**/*
public class UserInterface {
public static void main(String[] args) {

User u1 = new User();
u1.name = "Tom";
u1.password = "abc123";
Scanner scanner = new Scanner(System.in);
System.out.print("请输入用户名:");
String name = scanner.next();
System.out.print("请输入密码:");
String pwd = scanner.next();

*//**演示1**:*

*// u1.login(name,pwd);*

*//**演示2**:*
boolean isLogin = u1.login1(name, pwd);
if(isLogin){
System.out.println("登录成功:欢迎你," + u1.name);
}else{
System.out.println("登录失败:用户名或密码错误!");
}

scanner.close();

}
}

6. 对象数组

数组的元素可以是基本数据类型,也可以是引用数据类型。当元素是引用类型中的类时,我们称为对象数组。

1、案例

定义类Student,包含三个属性:学号number(int),年级state(int),成绩score(int)。 创建20个学生对象,学号为1到20,年级和成绩都由随机数确定。

问题一:打印出3年级(state值为3)的学生信息。

问题二:使用冒泡排序按学生成绩排序,并遍历所有学生信息

提示:

\1) 生成随机数:Math.random(),返回值类型double;

\2) 四舍五入取整:Math.round(double d),返回值类型long。

定义类Student,包含三个属性:学号number(int),年级state(int),成绩score(int)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
public class Student {

int number;*//**学号*
int state;*//**年级*
int score;*//**成绩*


public void info(){
System.out.println("number : " + number
\+ ",state : " + state + ",score : " + score);
}

}

public class StudentTest {

public static void main(String[] args) {

*// Student s1 = new Student();*
*// s1.number = 1;*
*// s1.state = (int)(Math.random() \* 6 + 1);//[1,6]*
*// s1.score = (int)(Math.random() \* 101);//[0,100]*
*//*
*// Student s2 = new Student();*
*// s2.number = 2;*
*// s2.state = (int)(Math.random() \* 6 + 1);//[1,6]*
*// s2.score = (int)(Math.random() \* 101);//[0,100]*
*//*
*// //....*
*//* *对象数组*
*// String[] arr = new String[10];*
*//* *数组的创建*
Student[] students = new Student[20];
*//* *通过循环结构给数组的属性赋值*
for (int i = 0; i < students.length; i++) {
*//* *数组元素的赋值*
students[i] = new Student();
*//* *数组元素是一个对象,给对象的各个属性赋值*
students[i].number = (i + 1);
students[i].state = (int) (Math.random() * 6 + 1);*// [1,6]*
students[i].score = (int) (Math.random() * 101);*// [0,100]*
}

*//* *问题一:打印出3**年级(state**值为3**)的学生信息。*
for (int i = 0; i < students.length; i++) {

if (students[i].state == 3) {

*// System.out.println(*
*// "number:" + students[i].number + ",state:" + students[i].state + ",score:" + students[i].score);*
students[i].info();


}

}
System.out.println("******************************");
*//* *问题二:使用冒泡排序按学生成绩排序,并遍历所有学生信息*
*//* *排序前*
for (int i = 0; i < students.length; i++) {

*// System.out.println(*
*// "number:" + students[i].number + ",state:" +*
*// students[i].state + ",score:" + students[i].score);*


students[i].info();
}

System.out.println();
*//* *排序:*
for (int i = 0; i < students.length - 1; i++) {
for (int j = 0; j < students.length - 1 - i; j++) {
if (students[j].score > students[j + 1].score) {
Student temp = students[j];
students[j] = students[j + 1];
students[j + 1] = temp;
}
}
}

*//* *排序后:*
for (int i = 0; i < students.length; i++) {

*// System.out.println(*
*// "number:" + students[i].number + ",state:" +*
*// students[i].state + ",score:" + students[i].score);*
students[i].info();
}
}
}

内存解析:

img

2、注意点

对象数组,首先要创建数组对象本身,即确定数组的长度,然后再创建每一个元素对象,如果不创建,数组的元素的默认值就是null,所以很容易出现空指针异常NullPointerException。

3、练习

(1)定义矩形类Rectangle,包含长、宽属性,area()返回矩形面积的方法,perimeter()返回矩形周长的方法,String getInfo()返回圆对象的详细信息(如:长、宽、面积、周长等数据)的方法

(2)在测试类中创建长度为3的Rectangle[]数组,用来装3个矩形对象,并给3个矩形对象的长分别赋值为10,20,30,宽分别赋值为5,15,25,遍历输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.atguigu.test08.array;

public class Rectangle {
double length;
double width;

public double area(){*//**面积*
return length * width;
}

public double perimeter(){*//**周长*
return 2 * (length + width);
}

public String getInfo(){
return "长:" + length +
",宽:" + width +
",面积:" + area() +
",周长:" + perimeter();
}
}

package com.atguigu.test08.array;

public class ObjectArrayTest {
public static void main(String[] args) {
*//**声明并创建一个长度为3**的矩形对象数组*
Rectangle[] array = new Rectangle[3];

*//**创建3**个矩形对象,并为对象的实例变量赋值,*
*//3**个矩形对象的长分别是10,20,30*
*//3**个矩形对象的宽分别是5,15,25*
*//**调用矩形对象的getInfo()**返回对象信息后输出*
for (int i = 0; i < array.length; i++) {
*//**创建矩形对象*
array[i] = new Rectangle();

*//**为矩形对象的成员变量赋值*
array[i].length = (i+1) * 10;
array[i].width = (2*i+1) * 5;

*//**获取并输出对象对象的信息*
System.out.println(array[i].getInfo());
}

}
}

内存解析:

img

7. 再谈方法

7.1 方法的重载(overload)

7.1.1 概念及特点

方法重载:在同一个类中,允许存在一个以上的同名方法,只要它们的参数列表不同即可。

– 参数列表不同,意味着参数个数或参数类型的不同

重载的特点:与修饰符、返回值类型无关,只看参数列表,且参数列表必须不同。(参数个数或参数类型)。调用时,根据方法参数列表的不同来区别。

重载方法调用:JVM通过方法的参数列表,调用匹配的方法。

– 先找个数、类型最匹配的

– 再找个数和类型可以兼容的,如果同时多个方法可以兼容将会报错

7.1.2 示例

举例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//System.out.println()**方法就是典型的重载方法,其内部的声明形式如下:*
public class PrintStream {
public void println(byte x)
public void println(short x)
public void println(int x)
public void println(long x)
public void println(float x)
public void println(double x)
public void println(char x)
public void println(double x)
public void println()

}

public class HelloWorld{
public static void main(String[] args) {
System.out.println(3);
System.out.println(1.2f);
System.out.println("hello!");
}
}

举例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//**返回两个整数的和*
public int add(int x,int y){
return x+y;
}

*//**返回三个整数的和*
public int add(int x,int y,int z){
return x+y+z;
}
*//**返回两个小数的和*
public double add(double x,double y){
return x+y;
}

举例3:方法的重载和返回值类型无关

public class MathTools {
*//**以下方法不是重载,会报错*
public int getOneToHundred(){
return (int)(Math.random()*100);
}

public double getOneToHundred(){
return Math.random()*100;
}
}

7.1.3 练习

练习1:判 断与void show(int a,char b,double c){}构成重载的有:

a)void show(int x,char y,double z){} // no

b)int show(int a,double c,char b){} // yes

c) void show(int a,double c,char b){} // yes

d) boolean show(int c,char b){} // yes

e) void show(double c){} // yes

f) double show(int x,char y,double z){} // no

g) void shows(){double c} // no

练习2:编写程序,定义三个重载方法并调用。

方法名为mOL。

三个方法分别接收一个int参数、两个int参数、一个字符串参数。分别执行平方运算并输出结果,相乘并输出结果,输出字符串信息。

在主类的main ()方法中分别用参数区别调用三个方法。

练习3:定义三个重载方法max(),第一个方法求两个int值中的最大值,第二个方法求两个double值中的最大值,第三个方法求三个double值中的最大值,并分别调用三个方法。

7.2 可变个数的形参

JDK 5.0 中提供了Varargs(variable number of arguments)机制。即当定义一个方法时,形参的类型可以确定,但是形参的个数不确定,那么可以考虑使用可变个数的形参。

格式:

1
方法名(参数的类型名 ...参数名)

举例:

1
2
3
4
5
//JDK 5.0**以前:采用数组形参来定义方法,传入多个同一类型变量*
public static void test(int a ,String[] books);

*//JDK5.0**:采用可变个数形参来定义方法,传入多个同一类型变量*
public static void test(int a ,String...books);

特点:

可变参数:方法参数部分指定类型的参数个数是可变多个:0个,1个或多个

可变个数形参的方法与同名的方法之间,彼此构成重载

可变参数方法的使用与方法参数部分使用数组是一致的,二者不能同时声明,否则报错。

方法的参数部分有可变形参,需要放在形参声明的最后

在一个方法的形参中,最多只能声明一个可变个数的形参

案例分析:

案例1:n个字符串进行拼接,每一个字符串之间使用某字符进行分割,如果没有传入字符串,那么返回空字符串””

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class StringTools {
String concat(char seperator, String... args){
String str = "";
for (int i = 0; i < args.length; i++) {
if(i==0){
str += args[i];
}else{
str += seperator + args[i];
}
}
return str;
}
}

package com.atguigu.test05.param;

public class StringToolsTest {
public static void main(String[] args) {
StringTools tools = new StringTools();

System.out.println(tools.concat('-'));
System.out.println(tools.concat('-',"hello"));
System.out.println(tools.concat('-',"hello","world"));
System.out.println(tools.concat('-',"hello","world","java"));

}
}

案例2:求n个整数的和

public class NumberTools {
public int total(int[] nums){
int sum = 0;
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
}
return sum;
}

public int sum(int... nums){
int sum = 0;
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
}
return sum;
}
}

public class TestVarParam {
public static void main(String[] args) {
NumberTools tools = new NumberTools();

System.out.println(tools.sum());*//0**个实参*
System.out.println(tools.sum(5));*//1**个实参*
System.out.println(tools.sum(5,6,2,4));*//4**个实参*
System.out.println(tools.sum(new int[]{5,6,2,4}));*//**传入数组实参*

System.out.println("------------------------------------");
System.out.println(tools.total(new int[]{}));*//0**个元素的数组*
System.out.println(tools.total(new int[]{5}));*//1**个元素的数组*
System.out.println(tools.total(new int[]{5,6,2,4}));*//**传入数组实参*

}
}

案例3:如下的方法彼此构成重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class MathTools {
*//**求两个整数的最大值*
public int max(int a,int b){
return a>b?a:b;
}

*//**求两个小数的最大值*
public double max(double a, double b){
return a>b?a:b;
}

*//**求三个整数的最大值*
public int max(int a, int b, int c){
return max(max(a,b),c);
}

*//**求n**个整数的最大值*
public int max(int... nums){
int max = nums[0];*//**如果没有传入整数,或者传入null**,这句代码会报异常*
for (int i = 1; i < nums.length; i++) {
if(nums[i] > max){
max = nums[i];
}
}
return max;
}
*/\* //**求n**整数的最大值*
*public int max(int[] nums){ //**编译就报错,与(int... nums)**无法区分*
*int max = nums[0];//**如果没有传入整数,或者传入null**,这句代码会报异常*
*for (int i = 1; i < nums.length; i++) {*
*if(nums[i] > max){*
*max = nums[i];*
*}*
*}*
*return max;*
*}\*/*

*/\* //**求n**整数的最大值*
*public int max(int first, int... nums){ //**当前类不报错,但是调用时会引起多个方法同时匹配*
*int max = first;*
*for (int i = 0; i < nums.length; i++) {*
*if(nums[i] > max){*
*max = nums[i];*
*}*
*}*
*return max;*
*}\*/*
}

7.3 方法的参数传递机制

7.3.1 形参和实参

形参(formal parameter):在定义方法时,方法名后面括号()中声明的变量称为形式参数,简称形参。

实参(actual parameter):在调用方法时,方法名后面括号()中的使用的值/变量/表达式称为实际参数,简称实参。

7.3.2 参数传递机制:值传递

Java里方法的参数传递方式只有一种:值传递。 即将实际参数值的副本(复制品)传入方法内,而参数本身不受影响。

形参是基本数据类型:将实参基本数据类型变量的“数据值”传递给形参

形参是引用数据类型:将实参引用数据类型变量的“地址值”传递给形参

7.3.3 举例

1、形参是基本数据类型

案例:编写方法,交换两个整型变量的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Test {
public static void main(String[] args) {
int m = 10;
int n = 20;


System.out.println("m = " + m + ", n = " + n);
*//**交换m**和n**的值*

*// int temp = m;*
*// m = n;*
*// n = temp;*


ValueTransferTest1 test = new ValueTransferTest1();
test.swap(m, n);

System.out.println("m = " + m + ", n = " + n);

}

public void swap(int m,int n){
int temp = m;
m = n;
n = temp;
}

}

内存解析:

img

2、形参是引用数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Test {
public static void main(String[] args) {


Data d1 = new Data();
d1.m = 10;
d1.n = 20;

System.out.println("m = " + d1.m + ", n = " + d1.n);

*//**实现* *换序*
ValueTransferTest2 test = new ValueTransferTest2();
test.swap(d1);
System.out.println("m = " + d1.m + ", n = " + d1.n);

}
public void swap(Data data){
int temp = data.m;
data.m = data.n;
data.n = temp;
}
}

class Data{
int m;
int n;
}

内存解析:

img

7.3.4 练习

练习1:判断如下程序输出的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class AssignNewObject {
public void swap(MyData my){
my = new MyData(); *//**考虑堆空间此新创建的对象,和main**中的data**对象是否有关*
int temp = my.x;
my.x = my.y;
my.y = temp;

}

public static void main(String[] args) {
AssignNewObject tools = new AssignNewObject();


MyData data = new MyData();
data.x = 1;
data.y = 2;
System.out.println("交换之前:x = " + data.x +",y = " + data.y);*//*
tools.swap(data);*//**调用完之后,x**与y**的值交换?*
System.out.println("交换之后:x = " + data.x +",y = " + data.y);*//*

}
}

class MyData{
int x ;
int y;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
练习2:如下操作是否可以实现数组排序

public class ArrayTypeParam {

*//**冒泡排序,实现数组从小到大排序*
public void sort(int[] arr){
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - 1 - i; j++) {
if(arr[j] > arr[j+1]){
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
*//**打印数组的元素*
public void print(int[] arr){
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]+" ");
}
System.out.println();
}

public static void main(String[] args) {
ArrayTypeParam tools = new ArrayTypeParam();

int[] nums = {4,3,1,6,7};
System.out.println("排序之前:");
tools.print(nums);

tools.sort(nums);*//**对nums**数组进行排序*

System.out.println("排序之后:");
tools.print(nums);*//**输出nums**数组的元素*

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
练习3:通过内存结构图,写出如下程序的输出结果

*//**栈:每个方法在调用时,都会有以栈帧的方法压入栈中。栈帧中保存了当前方法中声明的变量:方法内声明的,形参*
*//**堆:存放**new**出来的**"**东西**"**:对象(成员变量在对象中)、数组实体(数组元素)。*
*//**注意:变量前如果声明有类型,那么这就是一个新的刚要定义的变量。如果变量前没有声明类型,那就说明此变量在之前已经声明过。*
public class TransferTest3 {
public static void main(String args[]) {
TransferTest3 test = new TransferTest3();
test.first();
}
public void first() {
int i = 5;
Value v = new Value();
v.i = 25;
second(v, i);
System.out.println(v.i);
}
public void second(Value v, int i) {
i = 0;
v.i = 20;
Value val = new Value();
v = val;
System.out.println(v.i + " " + i);
}
}

class Value {
int i = 15;
}

内存解析:

20220320_233145

练习4:貌似是考查方法的参数传递

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
*//**法一:*
public static void method(int a, int b) {
*//* *在不改变原本题目的前提下,如何写这个函数才能在main**函数中输出a=100**,b=200**?*
a = a * 10;
b = b * 20;
System.out.println(a);
System.out.println(b);
System.exit(0);
}

*//**法二:*
public static void method(int a, int b) {

PrintStream ps = new PrintStream(System.out) {
@Override
public void println(String x) {

if ("a=10".equals(x)) {
x = "a=100";
} else if ("b=10".equals(x)) {
x = "b=200";
}
super.println(x);
}
};

System.setOut(ps);

}

练习5:将对象作为参数传递给方法

(1)定义一个Circle类,包含一个double型的radius属性代表圆的半径,一个findArea()方法返回圆的面积。 (2)定义一个类PassObject,在类中定义一个方法printAreas(),该方法的定义如下:public void printAreas(Circle c, int time),在printAreas方法中打印输出1到time之间的每个整数半径值,以及对应的面积。例如,times为5,则输出半径1,2,3,4,5,以及对应的圆面积。 (3)在main方法中调用printAreas()方法,调用完毕后输出当前半径值。程序运行结果如图所示。

img

7.4 递归(recursion)方法

举例1:

image-20220521160246138

举例2:

从前有座山,山上有座庙,庙里有个老和尚,老和尚在给小和尚讲故事,讲的啥?
从前有座山,山上有座庙,庙里有个老和尚,老和尚在给小和尚讲故事,讲的啥?
从前有座山,山上有座庙,庙里有个老和尚,老和尚在给小和尚讲故事,讲的啥?
从前有座山,山上有座庙,庙里有个老和尚,老和尚在给小和尚讲故事,讲的啥?…

老和尚没了,庙塌了,小和尚还俗结婚了。

递归方法调用:方法自己调用自己的现象就称为递归。

递归的分类: 直接递归、间接递归。

直接递归:方法自身调用自己。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 public void methodA(){
methodA();
}

间接递归:可以理解为A()方法调用B()方法,B()方法调用C()方法,C()方法调用A()方法。

public static void A(){
B();
}

public static void B(){
C();
}

public static void C(){
A();
}

说明

递归方法包含了一种隐式的循环。

递归方法会重复执行某段代码,但这种重复执行无须循环控制。

递归一定要向已知方向递归,否则这种递归就变成了无穷递归,停不下来,类似于死循环。最终发生栈内存溢出。

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
举例1:计算1 ~ n的和

public class RecursionDemo {
public static void main(String[] args) {
RecursionDemo demo = new RecursionDemo();
*//**计算1~num**的和,使用递归完成*
int num = 5;
*//* *调用求和的方法*
int sum = demo.getSum(num);
*//* *输出结果*
System.out.println(sum);

}
*/**
*通过递归算法实现.*
*参数列表:int*
*返回值类型: int*
**/*
public int getSum(int num) {
*/**
*num**为1**时,**方法返回1,*
*相当于是方法的出口,num**总有是1**的情况*
**/*
if(num == 1){
return 1;
}
*/**
*num**不为1**时,**方法返回 num +(num-1)**的累和*
*递归调用getSum**方法*
**/*
return num + getSum(num-1);
}
}

代码执行图解:

img

举例2:递归方法计算n!

1
2
3
4
5
6
7
public int multiply(int num){
if(num == 1){
return 1;
}else{
return num * multiply(num - 1);
}
}

img

举例3:已知有一个数列:f(0) = 1,f(1) = 4,f(n+2)=2*f(n+1) + f(n),其中n是大于0的整数,求f(10)的值。

1
2
3
4
5
6
7
8
9
public int f(int num){
if(num == 0){
return 1;
}else if(num == 1){
return 4;
}else{
return 2 * f(num - 1) + f(num - 2);
}
}

举例4:已知一个数列:f(20) = 1,f(21) = 4,f(n+2) = 2*f(n+1)+f(n),其中n是大于0的整数,求f(10)的值。

1
2
3
4
5
6
7
8
9
public int func(int num){
if(num == 20){
return 1;
}else if(num == 21){
return 4;
}else{
return func(num + 2) - 2 * func(num + 1);
}
}

举例5:计算斐波那契数列(Fibonacci)的第n个值,斐波那契数列满足如下规律,

1,1,2,3,5,8,13,21,34,55,….

即从第三个数开始,一个数等于前两个数之和。假设f(n)代表斐波那契数列的第n个值,那么f(n)满足: f(n) = f(n-2) + f(n-1);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//**使用递归的写法*
int f(int n) {*//**计算斐波那契数列第**n**个值是多少*
if (n < 1) {*//**负数是返回特殊值**1**,表示不计算负数情况*
return 1;
}
if (n == 1 || n == 2) {
return 1;
}
return f(n - 2) + f(n - 1);
}

*//**不用递归*
int fValue(int n) {*//**计算斐波那契数列第**n**个值是多少*
if (n < 1) {*//**负数是返回特殊值**1**,表示不计算负数情况*
return 1;
}
if (n == 1 || n == 2) {
return 1;
}
*//**从第三个数开始,* *等于* *前两个整数相加*
int beforeBefore = 1; *//**相当于**n=1**时的值*
int before = 1;*//**相当于**n=2**时的值*
int current = beforeBefore + before; *//**相当于**n=3**的值*
*//**再完后*
for (int i = 4; i <= n; i++) {
beforeBefore = before;
before = current;
current = beforeBefore + before;
*/**
*假设**i=4*
*beforeBefore = before; //**相当于**n=2**时的值*
*before = current; //**相当于**n=3**的值*
*current = beforeBefore + before; //**相当于**n = 4**的值*
*假设**i=5*
*beforeBefore = before; //**相当于**n=3**的值*
*before = current; //**相当于**n = 4**的值*
*current = beforeBefore + before; //**相当于**n = 5**的值*
*....*
**/*
}
return current;
}

举例6:面试题

宋老师,我今天去百度面试,遇到一个一个双重递归调用的问题,我琢磨了一下,完全不知道为什么。打断点了,也还是没看懂为什么程序会那样走。您有空可以看一下,求指教。

img

1
2
3
4
5
6
7
8
9
10
11
12
private int count = 0;

public int recursion(int k) {
count++;
System.out.println("count1:" + count + " k:" + k);
if (k <= 0) {
return 0;
}
return recursion(k - 1) + recursion(k - 2);*//287*
*//return recursion(k - 1);//11*
*//return recursion(k - 1) + recursion(k - 1);//2047*
}

剖析:

image-20220320235229941

最后说两句:

\1. 递归调用会占用大量的系统堆栈,内存耗用多,在递归调用层次多时速度要比循环慢的多,所以在使用递归时要慎重。

\2. 在要求高性能的情况下尽量避免使用递归,递归调用既花时间又耗内存。考虑使用循环迭代

8. 关键字:package、import

8.1 package(包)

package,称为包,用于指明该文件中定义的类、接口等结构所在的包。

8.1.1 语法格式

package 顶层包名.子包名 ;

举例:

1
2
3
4
5
6
7
8
9
pack1\pack2\PackageTest.java

package pack1.pack2; *//**指定类PackageTest**属于包pack1.pack2*

public class PackageTest{
public void display(){
System.out.println("in method display()");
}
}

说明:

• 一个源文件只能有一个声明包的package语句

• package语句作为Java源文件的第一条语句出现。若缺省该语句,则指定为无名包。

• 包名,属于标识符,满足标识符命名的规则和规范(全部小写)、见名知意

– 包通常使用所在公司域名的倒置:com.atguigu.xxx。

– 大家取包名时不要使用”java.xx”包

• 包对应于文件系统的目录,package语句中用 “.” 来指明包(目录)的层次,每.一次就表示一层文件目录。

• 同一个包下可以声明多个结构(类、接口),但是不能定义同名的结构(类、接口)。不同的包下可以定义同名的结构(类、接口)

8.1.2 包的作用

• 包可以包含类和子包,划分项目层次,便于管理

• 帮助管理大型软件系统:将功能相近的类划分到同一个包中。比如:MVC的设计模式

• 解决类命名冲突的问题

• 控制访问权限

8.1.3 应用举例

举例1:某航运软件系统包括:一组域对象、GUI和reports子系统

img

举例2:MVC设计模式

MVC是一种软件构件模式,目的是为了降低程序开发中代码业务的耦合度。

MVC设计模式将整个程序分为三个层次:视图模型(Viewer)层,控制器(Controller)层,与数据模型(Model)层。这种将程序输入输出、数据处理,以及数据的展示分离开来的设计模式使程序结构变的灵活而且清晰,同时也描述了程序各个对象间的通信方式,降低了程序的耦合性。

视图层viewer:显示数据,为用户提供使用界面,与用户直接进行交互。
>相关工具类 view.utils
>自定义view view.ui

控制层controller:解析用户请求,处理业务逻辑,给予用户响应
>应用界面相关 controller.activity
>存放fragment controller.fragment
>显示列表的适配器 controller.adapter
>服务相关的 controller.service
>抽取的基类 controller.base

模型层model:主要承载数据、处理数据
>数据对象封装 model.bean/domain
>数据库操作类 model.dao
>数据库 model.db

img

8.1.4 JDK中主要的包介绍

java.lang—-包含一些Java语言的核心类,如String、Math、Integer、 System和Thread,提供常用功能 java.net—-包含执行与网络相关的操作的类和接口。 java.io —-包含能提供多种输入/输出功能的类。 java.util—-包含一些实用工具类,如定义系统特性、接口的集合框架类、使用与日期日历相关的函数。 java.text—-包含了一些java格式化相关的类 java.sql—-包含了java进行JDBC数据库编程的相关类/接口 java.awt—-包含了构成抽象窗口工具集(abstract window toolkits)的多个类,这些类被用来构建和管理应用程序的图形用户界面(GUI)。

8.2 import(导入)

为了使用定义在其它包中的Java类,需用import语句来显式引入指定包下所需要的类。相当于import语句告诉编译器到哪里去寻找这个类。

8.2.1 语法格式

1
import 包名.类名;

8.2.2 应用举例

1
2
3
4
5
6
7
8
import pack1.pack2.Test;  *//import pack1.pack2.\*;**表示引入pack1.pack2**包中的所有结构*

public class PackTest{
public static void main(String args[]){
Test t = new Test(); *//Test**类在pack1.pack2**包中定义*
t.display();
}
}

8.2.3 注意事项

• import语句,声明在包的声明和类的声明之间。

• 如果需要导入多个类或接口,那么就并列显式多个import语句即可

• 如果使用a.导入结构,表示可以导入a包下的所有的结构。举例:可以使用java.util.的方式,一次性导入util包下所有的类或接口。

• 如果导入的类或接口是java.lang包下的,或者是当前包下的,则可以省略此import语句。

• 如果已经导入java.a包下的类,那么如果需要使用a包的子包下的类的话,仍然需要导入。

• 如果在代码中使用不同包下的同名的类,那么就需要使用类的全类名的方式指明调用的是哪个类。

• (了解)import static组合的使用:调用指定类或接口下的静态的属性或方法

9. 面向对象特征一:封装性(encapsulation)

9.1 为什么需要封装?

• 我要用洗衣机,只需要按一下开关和洗涤模式就可以了。有必要了解洗衣机内部的结构吗?有必要碰电动机吗?

• 我要开车,我不需要懂离合、油门、制动等原理和维修也可以驾驶。

• 客观世界里每一个事物的内部信息都隐藏在其内部,外界无法直接操作和修改,只能通过指定的方式进行访问和修改。

随着我们系统越来越复杂,类会越来越多,那么类之间的访问边界必须把握好,面向对象的开发原则要遵循“高内聚、低耦合”。

高内聚、低耦合是软件工程中的概念,也是UNIX 操作系统设计的经典原则。

内聚,指一个模块内各个元素彼此结合的紧密程度;耦合指一个软件结构内不同模块之间互连程度的度量。内聚意味着重用和独立,耦合意味着多米诺效应牵一发动全身。

而“高内聚,低耦合”的体现之一:

• 高内聚:类的内部数据操作细节自己完成,不允许外部干涉;

• 低耦合:仅暴露少量的方法给外部使用,尽量方便外部调用。

9.2 何为封装性?

所谓封装,就是把客观事物封装成抽象概念的类,并且类可以把自己的数据和方法只向可信的类或者对象开放,向没必要开放的类或者对象隐藏信息。

通俗的讲,把该隐藏的隐藏起来,该暴露的暴露出来。这就是封装性的设计思想。

9.3 Java如何实现数据封装

• 实现封装就是控制类或成员的可见性范围。这就需要依赖访问控制修饰符,也称为权限修饰符来控制。

• 权限修饰符:public、protected、缺省、private。具体访问范围如下:

修饰符 本类内部 本包内 其他包的子类 其他包非子类
private × × ×
缺省 × ×
protected ×
public

• 具体修饰的结构:

– 外部类:public、缺省

– 成员变量、成员方法、构造器、成员内部类:public、protected、缺省、private

image-20220321222327616

image-20220617164042390

9.4 封装性的体现

9.4.1 成员变量/属性私有化

概述:私有化类的成员变量,提供公共的get和set方法,对外暴露获取和修改属性的功能。

实现步骤:

使用 private 修饰成员变量

private 数据类型 变量名 ;

代码如下:

1
2
3
4
5
public class Person {
private String name;
private int age;
private boolean marry;
}

提供 getXxx方法 / setXxx 方法,可以访问成员变量,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Person {
private String name;
private int age;
private boolean marry;

public void setName(String n) {
name = n;
}

public String getName() {
return name;
}

public void setAge(int a) {
age = a;
}

public int getAge() {
return age;
}

public void setMarry(boolean m){
marry = m;
}

public boolean isMarry(){
return marry;
}
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class PersonTest {
public static void main(String[] args) {
Person p = new Person();

*//**实例变量私有化,跨类是无法直接使用的*
*/\* p.name = "**张三";*
*p.age = 23;*
*p.marry = true;\*/*

p.setName("张三");
System.out.println("p.name = " + p.getName());

p.setAge(23);
System.out.println("p.age = " + p.getAge());

p.setMarry(true);
System.out.println("p.marry = " + p.isMarry());

}
}

成员变量封装的好处:

• 让使用者只能通过事先预定的方法来访问数据,从而可以在该方法里面加入控制逻辑,限制对成员变量的不合理访问。还可以进行数据检查,从而有利于保证对象信息的完整性。

• 便于修改,提高代码的可维护性。主要说的是隐藏的部分,在内部修改了,如果其对外可以的访问方式不变的话,外部根本感觉不到它的修改。例如:Java8->Java9,String从char[]转为byte[]内部实现,而对外的方法不变,我们使用者根本感觉不到它内部的修改。

开心一笑:

A man and woman are in a computer programming lecture. The man touches the woman’s breasts.

“Hey!” she says. “Those are private!”

The man says, “But we’re in the same class!”

9.4.2 私有化方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
public class ArrayUtil {

public int max(int[] arr) {
int maxValue = arr[0];
for(int i = 1;i < arr.length;i++){
if(maxValue < arr[i]){
maxValue = arr[i];
}
}
return maxValue;

}


public int min(int[] arr){
int minValue = arr[0];
for(int i = 1;i < arr.length;i++){
if(minValue > arr[i]){
minValue = arr[i];
}
}
return minValue;
}


public int sum(int[] arr) {
int sum = 0;
for(int i = 0;i < arr.length;i++){
sum += arr[i];
}
return sum;
}


public int avg(int[] arr) {
int sumValue = sum(arr);
return sumValue / arr.length;
}

*//* *创建一系列重载的上述方法*
*// public double max(double[] arr){}*
*// public float max(float[] arr){}*
*// public byte max(byte[] arr){}*

public void print(int[] arr) {
for(int i = 0;i < arr.length;i++){
System.out.print(arr[i] + " ");
}
System.out.println();

}

public int[] copy(int[] arr) {
int[] arr1 = new int[arr.length];
for(int i = 0;i < arr.length;i++){
arr1[i] = arr[i];
}
return arr1;

}

public void reverse(int[] arr) {
for(int i = 0,j = arr.length - 1;i < j;i++,j--){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}

public void sort(int[] arr,String desc) {


if("ascend".equals(desc)){*//if(desc.equals("ascend")){*
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {

*// int temp = arr[j];*
*// arr[j] = arr[j + 1];*
*// arr[j + 1] = temp;*
swap(arr,j,j+1);
}
}
}
}else if ("descend".equals(desc)){
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] < arr[j + 1]) {
*// int temp = arr[j];*
*// arr[j] = arr[j + 1];*
*// arr[j + 1] = temp;*
swap(arr,j,j+1);
}
}
}
}else{
System.out.println("您输入的排序方式有误!");
}
}

private void swap(int[] arr,int i,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}

*/***
***
*** *@*param arr
*** *@*param value
*** *@*return *返回*value*值出现的位置* *或* *-1**:未找到*
**/*
public int getValue(int[] arr, int value) {
*//**方法:线性查找*
for(int i = 0;i < arr.length;i++){
if(value == arr[i]){
return i;
}
}


return - 1;

}
}

注意:

开发中,一般成员实例变量都习惯使用private修饰,再提供相应的public权限的get/set方法访问。

对于final的实例变量,不提供set()方法。(后面final关键字的时候讲)

对于static final的成员变量,习惯上使用public修饰。

9.5 练习

练习1:

创建程序:在其中定义两个类:Person和PersonTest类。定义如下:

用setAge()设置人的合法年龄(0~130),用getAge()返回人的年龄。在PersonTest类中实例化Person类的对象b,调用setAge()和getAge()方法,体会Java的封装性。

image-20220321223633916

练习2:

自定义图书类。设定属性包括:书名bookName,作者author,出版社名publisher,价格price;方法包括:相应属性的get/set方法,图书信息介绍等。

10. 类的成员之三:构造器(Constructor)

我们new完对象时,所有成员变量都是默认值,如果我们需要赋别的值,需要挨个为它们再赋值,太麻烦了。我们能不能在new对象时,直接为当前对象的某个或所有成员变量直接赋值呢?

可以,Java给我们提供了构造器(Constructor),也称为构造方法。

10.1 构造器的作用

new对象,并在new对象的时候为实例变量赋值。

举例:Person p = new Person(“Peter”,15);

解释:如同我们规定每个“人”一出生就必须先洗澡,我们就可以在“人”的构造器中加入完成“洗澡”的程序代码,于是每个“人”一出生就会自动完成“洗澡”,程序就不必再在每个人刚出生时一个一个地告诉他们要“洗澡”了。

10.2 构造器的语法格式

1
2
3
4
5
6
7
8
[修饰符] class 类名{
[修饰符] 构造器名(){
*//* *实例初始化代码*
}
[修饰符] 构造器名(参数列表){
*//* *实例初始化代码*
}
}

说明:

\3. 构造器名必须与它所在的类名必须相同。

\4. 它没有返回值,所以不需要返回值类型,也不需要void。

\5. 构造器的修饰符只能是权限修饰符,不能被其他任何修饰。比如,不能被static、final、synchronized、abstract、native修饰,不能有return语句返回值。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class Student {
private String name;
private int age;

*//* *无参构造*
public Student() {}

*//* *有参构造*
public Student(String n,int a) {
name = n;
age = a;
}

public String getName() {
return name;
}
public void setName(String n) {
name = n;
}
public int getAge() {
return age;
}
public void setAge(int a) {
age = a;
}

public String getInfo(){
return "姓名:" + name +",年龄:" + age;
}
}

public class TestStudent {
public static void main(String[] args) {
*//**调用无参构造创建学生对象*
Student s1 = new Student();

*//**调用有参构造创建学生对象*
Student s2 = new Student("张三",23);

System.out.println(s1.getInfo());
System.out.println(s2.getInfo());

}
}

10.3 使用说明

当我们没有显式的声明类中的构造器时,系统会默认提供一个无参的构造器并且该构造器的修饰符默认与类的修饰符相同

img

当我们显式的定义类的构造器以后,系统就不再提供默认的无参的构造器了。

在类中,至少会存在一个构造器。

构造器是可以重载的。

10.4 练习

练习1:编写两个类,TriAngle和TriAngleTest,其中TriAngle类中声明私有的底边长base和高height,同时声明公共方法访问私有变量。此外,提供类必要的构造器。另一个类中使用这些公共方法,计算三角形的面积。

练习2:

(1)定义Student类,有4个属性: String name; int age; String school; String major;

(2)定义Student类的3个构造器:

• 第一个构造器Student(String n, int a)设置类的name和age属性;

• 第二个构造器Student(String n, int a, String s)设置类的name, age 和school属性;

• 第三个构造器Student(String n, int a, String s, String m)设置类的name, age ,school和major属性;

(3)在main方法中分别调用不同的构造器创建的对象,并输出其属性值。

练习3:

1、写一个名为Account的类模拟账户。该类的属性和方法如下图所示。

该类包括的属性:账号id,余额balance,年利率annualInterestRate;

包含的方法:访问器方法(getter和setter方法),取款方法withdraw(),存款方法deposit()。

标题: fig:

提示:在提款方法withdraw中,需要判断用户余额是否能够满足提款数额的要求,如果不能,应给出提示。

\1. 创建Customer类。

标题: fig:

\2. 声明三个私有对象属性:firstName、lastName和account。 b. 声明一个公有构造器,这个构造器带有两个代表对象属性的参数(f和l) c. 声明两个公有存取器来访问该对象属性,方法getFirstName和getLastName返回相应的属性。 d. 声明setAccount 方法来对account属性赋值。 e. 声明getAccount 方法以获取account属性。

3.写一个测试程序。

(1)创建一个Customer ,名字叫 Jane Smith, 他有一个账号为1000,余额为2000元,年利率为 1.23% 的账户。 (2)对Jane Smith操作。 存入 100 元,再取出960元。再取出2000元。 打印出Jane Smith 的基本信息

成功存入 :100.0
成功取出:960.0
余额不足,取款失败
Customer [Smith, Jane] has a account: id is 1000, annualInterestRate is 1.23%, balance is 1140.0

11. 阶段性知识补充

11.1 类中属性赋值过程

1、在类的属性中,可以有哪些位置给属性赋值?

① 默认初始化

② 显式初始化

③ 构造器中初始化

④ 通过”对象.属性”或”对象.方法”的方式,给属性赋值

2、这些位置执行的先后顺序是怎样?

顺序:① - ② - ③ - ④

3、说明:

上述中的①、②、③在对象创建过程中,只执行一次。

④ 是在对象创建后执行的,可以根据需求多次执行。

11.2 JavaBean

JavaBean是一种Java语言写成的可重用组件。

– 好比你做了一个扳手,这个扳手会在很多地方被拿去用。这个扳手也提供多种功能(你可以拿这个扳手扳、锤、撬等等),而这个扳手就是一个组件。

所谓JavaBean,是指符合如下标准的Java类:

  • – 类是公共的

  • – 有一个无参的公共的构造器

  • – 有属性,且有对应的get、set方法

• 用户可以使用JavaBean将功能、处理、值、数据库访问和其他任何可以用Java代码创造的对象进行打包,并且其他的开发者可以通过内部的JSP页面、Servlet、其他JavaBean、applet程序或者应用来使用这些对象。用户可以认为JavaBean提供了一种随时随地的复制和粘贴的功能,而不用关心任何改变。

• 《Think in Java》中提到,JavaBean最初是为Java GUI的可视化编程实现的。你拖动IDE构建工具创建一个GUI 组件(如多选框),其实是工具给你创建Java类,并提供将类的属性暴露出来给你修改调整,将事件监听器暴露出来。

• 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  public class JavaBean {
private String name; *//* *属性一般定义为private*
private int age;
public JavaBean() {
}
public int getAge() {
return age;
}
public void setAge(int a) {
age = a;
}
public String getName() {
return name;
}
public void setName(String n) {
name = n;
}
}

11.3 UML类图

UML(Unified Modeling Language,统一建模语言),用来描述软件模型和架构的图形化语言。

• 常用的UML工具软件有PowerDesinger、Rose和Enterprise Architect。

• UML工具软件不仅可以绘制软件开发中所需的各种图表,还可以生成对应的源代码。

• 在软件开发中,使用UML类图可以更加直观地描述类内部结构(类的属性和操作)以及类之间的关系(如关联、依赖、聚合等)。

– +表示 public 类型, - 表示 private 类型,#表示protected类型

– 方法的写法: 方法的类型(+、-) 方法名(参数名: 参数类型):返回值类型

– 斜体表示抽象方法或类。

image-20220321232230831

img

第07章_面向对象编程(进阶)


本章专题与脉络

第2阶段:Java面向对象编程-第07章

1. 关键字:this

1.1 this是什么?

  • 在Java中,this关键字不算难理解,它的作用和其词义很接近。

    • 它在方法(实例方法/非static的方法)内部使用,表示调用该方法的对象 —this.name=name;这时候
    • 它在构造器内部使用,表示该构造器正在初始化的对象。 – -Person person=new Person();这时候
  • this可以调用的结构:成员变量[不可以是局部变量!!]、方法和构造器

1.2 什么时候使用this

1.2.1 实例方法/构造器中—使用当前对象的成员(区分成员变量和传入的局部变量)

在实例方法或构造器中,如果使用当前类的成员变量或成员方法可以在其前面添加this,增强程序的可读性。不过,通常我们都习惯省略this。

但是,当形参与成员变量同名时,如果在方法内或构造器内需要使用成员变量,必须添加this来表明该变量是类的成员变量。即:我们可以用this来区分成员变量局部变量。比如:

image-20220503102947013

另外,使用this访问属性和方法时,如果在本类中未找到,会从父类中查找。这个在继承中会讲到。

举例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person{		// 定义Person类
private String name ;
private int age ;
public Person(String name,int age){
this.name = name ;
this.age = age ;
}
public void setName(String name){
this.name = name;
}
public void setAge(int age){
this.age = age;
}
public void getInfo(){
System.out.println("姓名:" + name) ;
this.speak();
}
public void speak(){
System.out.println(“年龄:” + this.age);
}
}

举例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Rectangle {
int length;
int width;

public int area() {
return this.length * this.width;
}

public int perimeter(){
return 2 * (this.length + this.width);
}

public void print(char sign) {
for (int i = 1; i <= this.width; i++) {
for (int j = 1; j <= this.length; j++) {
System.out.print(sign);
}
System.out.println();
}
}

public String getInfo(){
return "长:" + this.length + ",宽:" + this.width +",面积:" + this.area() +",周长:" + this.perimeter();
}
}

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class TestRectangle {
public static void main(String[] args) {
Rectangle r1 = new Rectangle();
Rectangle r2 = new Rectangle();

System.out.println("r1对象:" + r1.getInfo());
System.out.println("r2对象:" + r2.getInfo());

r1.length = 10;
r1.width = 2;
System.out.println("r1对象:" + r1.getInfo());
System.out.println("r2对象:" + r2.getInfo());

r1.print('#');
System.out.println("---------------------");
r1.print('&');

System.out.println("---------------------");
r2.print('#');
System.out.println("---------------------");
r2.print('%');
}
}

1.2.2 同一个类中—-构造器互相调用(减少重复代码)

this可以作为一个类中构造器相互调用的特殊格式。 –我们可以通过this调用类中的其他构造器!!!!

  • this():调用本类的无参构造器

  • this(实参列表):调用本类的有参构造器

    image-20230927165025075

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Student {
private String name;
private int age;
// 无参构造
public Student() {
// this("",18);//调用本类有参构造器
}

// 有参构造
public Student(String name) {
this();//调用本类无参构造器 必须是首行 只能最多声明一个this()
this.name = name;
}
// 有参构造
public Student(String name,int age){
//直接this.name=name都可以省略了
this(name);//调用本类中有一个String参数的构造器 必须是首行 只能最多声明一个this()
this.age = age;
}
}

注意:

  • 不能出现递归调用。比如,调用自身构造器。
    • 推论:如果一个类中声明了n个构造器,则最多有 n - 1个构造器中使用了”this(形参列表)
  • this()和this(实参列表)只能声明在构造器首行
    • 推论:在类的一个构造器中,最多只能声明一个”this(参数列表)

1.3 练习

练习1:添加必要的构造器,综合应用构造器的重载,this关键字。

image-20220808191154534

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class Boy {
private String name; //name和age前面写的负号 说明是私有属性
private String age;

public void setAge(String age){ //set方法传入的就可以是age 使用this.age=age可以区分传入的局部变量
this.age=age;
}

public void setName(String name){ //set方法传入的就可以是name 使用this.name=name可以区分传入的局部变量
this.name=name;
}

public String getAge(){
return this.age; //this可以省略
}

public String getName(){
return this.name; //this可以省略
}

public void marry(Girl girl){

}

public void shout(){

}

public Boy(){

}

public Boy(String name,String age){
this.name=name; //this()可以调用无参构造器
this.age=age;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Girl {
private String name; //name和age前面写的负号 说明是私有属性
private String age;

public void setAge(String age){ //set方法传入的就可以是age 使用this.age=age可以区分传入的局部变量
this.age=age;
}

public void setName(String name){ //set方法传入的就可以是name 使用this.name=name可以区分传入的局部变量
this.name=name;
}

public String getAge(){
return this.age; //this可以省略
}

public String getName(){
return this.name; //this可以省略
}

public void marry(Boy boy){

}

public void compare(Girl girl){

}
}

练习2:

(1)按照如下的UML类图,创建相应的类,提供必要的结构:

image-20220323211412409

在提款方法withdraw()中,需要判断用户余额是否能够满足提款数额的要求,如果不能,应给出提示。deposit()方法表示存款。

(2)按照如下的UML类图,创建相应的类,提供必要的结构

image-20220323211454372

(3)按照如下的UML类图,创建相应的类,提供必要的结构

image-20220323211521808
  • addCustomer 方法必须依照参数(姓,名)构造一个新的 Customer对象,然后把它放到 customer 数组中。还必须把 numberOfCustomer 属性的值加 1。

  • getNumOfCustomers 方法返回 numberofCustomers 属性值。

  • getCustomer方法返回与给出的index参数相关的客户。

(4)创建BankTest类,进行测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Account {
private double balance; //因为balance前面是负号

//前面一个加号是public 加横线了就是构造器
public Account(double balance){
this.balance=balance; //使用this区分成员变量和局部变量
}

//前面一个加号是public 冒号后面是double是返回值
public double getBalance(){
return this.balance; //this可以省略
}

public void deposit(double amt){ //存钱
if(amt>0){
this.balance+=amt;
System.out.println("存入成功!!!");
}
}

public void withdraw(double amt){ //取钱
if(amt>this.balance){
System.out.println("余额不足!!!");
return; //跳出结果
}
this.balance-=amt;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Customer {
private String firstName; //三个属性都是负号 所以设定为私有private
private String lastName; //三个属性都是负号 所以设定为私有private
private Account account; //三个属性都是负号 所以设定为私有private

//正号所以是public 下划线所以是构造器
public Customer(String f,String l){
this.firstName=f;
this.lastName=l;
}

//有冒号 说明有返回值
public String getFirstName(){
return this.firstName;
}

//有冒号 说明有返回值
public String getLastName(){
return this.lastName;
}

//有冒号 说明有返回值
public Account getAccount(){
return this.account;
}

//没有冒号 说明没有返回值
public void setAccount(Account acct){
this.account=acct;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Bank {
private Customer[] customers; //私有化不能get和set创建
private int numberOfCustomer;

public Bank(){
customers = new Customer[10]; //要初始化 不然BankTest会出现customers是null!!!
}

public void addCustomer(String f,String l){
Customer customer=new Customer(f,l); //新创建的顾客
customers[this.numberOfCustomer++]=customer; //将顾客添加到顾客数组
}

public int getNumberOfCustomer(){
return this.numberOfCustomer;
}

public Customer getCustomer(int index){
if(index < 0 || index >= numberOfCustomer){
return null;
}
return customers[index]; //返回对应位置的顾客
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BankTest {
public static void main(String[] args) {
Bank bank = new Bank();

bank.addCustomer("操","曹");
bank.addCustomer("备","刘");

bank.getCustomer(0).setAccount(new Account(1000)); //银行类bank获取第一个顾客,顾客设定一个账户,账户里面有1000余额
bank.getCustomer(0).getAccount().withdraw(50); //余额取出50

System.out.println("账户余额为:" + bank.getCustomer(0).getAccount().getBalance()); //获取余额950

}
}

内存解析图:

image-20220323211653907

2. 面向对象特征二:继承(Inheritance)

2.1 继承的概述

角度一:从上而下

为描述和处理个人信息,定义类Person:

image-20220323220923386

为描述和处理学生信息,定义类Student:

image-20220323221001495

通过继承,简化Student类的定义:

image-20220323221050791

说明:Student类继承了父类Person的所有属性和方法,并增加了一个属性school。Person中的属性和方法,Student都可以使用。

角度二:从下而上

多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类中无需再定义这些属性和行为,只需要和抽取出来的类构成继承关系。如图所示:

再举例:

image-20220323221436571

2.1.1 继承的好处

  • 继承的出现减少了代码冗余,提高了代码的复用性。

  • 继承的出现,更有利于功能的扩展。

  • 继承的出现让类与类之间产生了is-a的关系,为多态的使用提供了前提。

    • 继承描述事物之间的所属关系,这种关系是:is-a 的关系。可见,父类更通用、更一般,子类更具体。

注意:不要仅为了获取其他类中某个功能而去继承!

2.2 继承的语法

2.2.1 继承中的语法格式

通过 extends 关键字,可以声明一个类B继承另外一个类A,定义格式如下:

1
2
3
4
5
6
7
[修饰符] classA {  //类A,称为父类、超类、基类(base class)、SuperClass
//属性和方法
}

[修饰符] classB extendsA { //类B,称为子类、派生类(derived class)、SubClass
...
}

2.2.2 继承中的基本概念

类B,称为子类、派生类(derived class)、SubClass

类A,称为父类、超类、基类(base class)、SuperClass

2.3 代码举例

1、父类

1
2
3
4
5
6
7
8
9
10
public class Animal {
// 定义name属性
String name;
// 定义age属性
int age;
// 定义动物的吃东西方法
public void eat() {
System.out.println(age + "岁的"+ name + "在吃东西");
}
}

2、子类

1
2
3
4
5
6
7
8
public class Cat extends Animal {  //使用extends继承   
int count;//记录每只猫抓的老鼠数量
// 定义一个猫抓老鼠的方法catchMouse
public void catchMouse() {
count++;
System.out.println("抓老鼠,已经抓了"+ count + "只老鼠");
}
}

3、测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TestCat {
public static void main(String[] args) {
// 创建一个猫类对象
Cat cat = new Cat();
// 为该猫类对象的name属性进行赋值 这是父类Animal的属性和方法(继承过来的)
cat.name = "Tom";
// 为该猫类对象的age属性进行赋值
cat.age = 2;
// 调用该猫继承来的eat()方法
cat.eat();
// 调用该猫的catchMouse()方法 这是自己类的方法
cat.catchMouse();
}
}

image-20211230090255997

2.4 继承性的细节说明

1、子类会继承父类所有的实例变量和实例方法

从类的定义来看,类是一类具有相同特性的事物的抽象描述。父类是所有子类共同特征的抽象描述。而实例变量和实例方法就是事物的特征,那么父类中声明的实例变量和实例方法代表子类事物也有这个特征。

  • 当子类对象被创建时,在堆中给new对象申请内存时,就要看子类和父类都声明了什么实例变量,这些实例变量都要分配内存
  • 当子类对象调用方法时,编译器会先在子类模板中看该类是否有这个方法,如果没找到,父类—->父类—->父类是否声明了这个方法,遵循从下往上找的顺序,找到了就停止,一直到根父类都没有找到,就会报编译错误。

所以继承意味着子类的对象除了看子类的类模板还要看父类的类模板。

image-20211230090255997

2、子类不能直接访问父类中私有的(private)的成员变量和方法

子类虽会继承父类私有(private)的成员变量,但子类不能对继承的私有成员变量直接进行访问,——>通过get/set方法进行访问。 (继承性要保证封装性!!!)

如图所示:

image-20220323224757212

3、在Java 中,继承的关键字用的是“extends”,即子类不是父类的子集,而是对父类的“扩展”

子类在继承父类以后,还可以定义自己特有的方法,这就可以看做是对父类功能上的扩展。

4、Java支持多层继承(继承体系)

image-20220323225441417
1
2
3
class A{}
class B extends A{}
class C extends B{}

说明:

  • 子类和父类是一种相对的

  • 顶层父类是Object类。所有的类默认继承Object,作为父类。 (如果没有显示说明的话就是继承Object类)

5、一个父类可以同时拥有多个子类

1
2
3
4
class A{}
class B extends A{}
class D extends A{}
class E extends A{}

6、Java只支持单继承,不支持多重继承

image-20220514162507692
1
2
3
4
5
6
public class A{}
class B extends A{}

//一个类只能有一个父类,不可以有多个直接父类。
class C extends B{} // √
class C extends A,B... // ×

2.5 练习

练习1:定义一个学生类Student,它继承自Person类

image-20220323231804928
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Person {
public String name;
public char sex;
public int age;

public Person(String name,char sex,int age){ //构造器
this.name=name;
this.sex=sex;
this.age=age;
}

public String toString(){
return "name是: "+this.name+"sex是: "+this.sex+" age是: "+this.age;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Student extends Person{
public long number;
public int math;
public int english;
public int computer;

public Student(String n,char s,int a,long k,int m,int e,int c){
super(n,s,a); //要使用super调用父类的三个属性 跟this(有参/无参)功能样式一样!!!
this.number=k;
this.math=m;
this.english=e;
this.computer=c;
}

public double aver(){
return 1.0;
}

public int max(){
return 1;
}

public int min(){
return 1;
}

}

练习2:

(1)定义一个ManKind类,包括

  • 成员变量int sex和int salary;

  • 方法void manOrWoman():根据sex的值显示“man”(sex==1)或者“woman”(sex==0);

  • 方法void employeed():根据salary的值显示“no job”(salary==0)或者“ job”(salary!=0)。

(2)定义类Kids继承ManKind,并包括

  • 成员变量int yearsOld;
  • 方法printAge()打印yearsOld的值。

(3)定义类KidsTest,在类的main方法中实例化Kids的对象someKid,用该对象访问其父类的成员变量及方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class ManKind {
private int sex; //属性设置为私有 子类只能通过get/set方法获取 [不能直接获取]
private int salary; //属性设置为私有 子类只能通过get/set方法获取 [不能直接获取]

public void manOrWoman(){ //我的误区就是经常往里面传值,其实成员变量没必要,传入的是局部变量!!!
if(sex==1){
System.out.println("man");
return;
}
if(sex==0){
System.out.println("woman");
return;
}
System.out.println("输入sex异常");
}

public int getSex() {
return sex;
}

public void setSex(int sex) {
this.sex = sex;
}

public int getSalary() {
return salary;
}

public void setSalary(int salary) {
this.salary = salary;
}

//无参构造器
public ManKind() {
}

//有参构造器
public ManKind(int sex, int salary) {
this(); //调用无参构造器public ManKind(){}
this.sex = sex;
this.salary = salary;
}

public void employeed(){ //我的误区就是经常往里面传值,其实成员变量没必要,传入的是局部变量!!!
if(salary==0){
System.out.println("no job");
return;
}
System.out.println("job");
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Kids extends ManKind{  //使用extends关键字继承父类
private int yearsOld; //属性设置成私有 其他人获取只能通过get和set方法 而且private只能在类中和同一包内

public int getYearsOld() {
return yearsOld;
}

public void setYearsOld(int yearsOld) {
this.yearsOld = yearsOld;
}

public void printAge(){
System.out.println(this.yearsOld);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class KidsTest {
public static void main(String[] args) {
Kids someKid=new Kids();
//调用父类属性
someKid.setSex(1);
someKid.setSalary(1); //相当于子类拥有了父类和自己的属性 三个属性一共
//调用父类方法
someKid.manOrWoman();
someKid.employeed();
}
}


输出:
man
job

练习3:根据下图实现类。在CylinderTest类中创建Cylinder类的对象,设置圆柱的底面半径和高,并输出圆柱的体积。

image-20220323231942361
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Circle {
private double radius;
public Circle(){
this.radius=1; //初始化为1
}

//声明set和get方便其他类访问private成员变量
public void setRadius(double radius){
this.radius=radius;
}

public double getRadius(){
return this.radius;
}

public double findArea(){
return this.radius*this.radius*Math.PI; //计算圆的面积
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Cylinder extends Circle{  //使用extends继承父类圆Circle
private double length;

public Cylinder(){
this.length=1; //初始化为1
}

//声明set和get方便其他类访问private成员变量
public void setLength(double length) {
this.length = length;
}

public double getLength(){
return this.length;
}

public double findVolume(){
return this.findArea()*length; //或者getLength()
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class CylinderTest {
public static void main(String[] args) {
Cylinder cy = new Cylinder(); //创建一个圆柱对象 子类
cy.setRadius(2.3); //子类使用父类的属性
cy.setLength(1.4); //子类使用自己的方法
System.out.println("圆柱的体积为:" + cy.findVolume());
System.out.println("圆柱的底面积:" + cy.findArea()); //子类使用父类的方法
}
}

输出:
圆柱的体积为:23.266635192486003
圆柱的底面积:16.619025137490002

3. 方法的重写(override/overwrite)

父类的所有方法子类都会继承,但是当某个方法被继承到子类之后,子类觉得父类原来的实现不适合于自己当前的类,该怎么办呢?子类可以对从父类中继承来的方法进行改造,我们称为方法的重写 (override、overwrite)。也称为方法的重置覆盖

在程序执行时,子类的方法将覆盖父类的方法。

3.1 方法重写举例

比如新的手机增加来电显示头像的功能,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.atguigu.inherited.method;

public class Phone {
public void sendMessage(){
System.out.println("发短信");
}
public void call(){
System.out.println("打电话");
}
public void showNum(){
System.out.println("来电显示号码");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.atguigu.inherited.method;

//SmartPhone:智能手机
public class SmartPhone extends Phone{
//重写父类的来电显示功能的方法
@Override
public void showNum(){
//来电显示姓名和图片功能
System.out.println("显示来电姓名");
System.out.println("显示头像");
}
//重写父类的通话功能的方法
@Override
public void call() {
System.out.println("语音通话 或 视频通话");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.atguigu.inherited.method;

public class TestOverride {
public static void main(String[] args) {
// 创建子类对象
SmartPhone sp = new SmartPhone();

// 调用父类继承而来的方法
sp.call();

// 调用子类重写的方法
sp.showNum();
}
}

@Override使用说明:

写在方法上面,用来检测是不是满足重写方法的要求。这个注解就算不写,只要满足要求,也是正确的方法覆盖重写。建议保留,这样编译器可以帮助我们检查格式,另外也可以让阅读源代码的程序员清晰的知道这是一个重写的方法。

3.2 方法重写的要求

  1. 子类重写的方法必须和父类被重写的方法具有相同的方法名称参数列表

  2. 子类重写的方法的返回值类型不能大于父类被重写的方法的返回值类型。(例如:Student < Person)。

注意:如果返回值类型是基本数据类型和void,那么必须是相同

  1. 子类重写的方法使用的访问权限不能小于父类被重写的方法的访问权限。(public > protected > 缺省 > private)

注意:① 父类私有方法不能重写 ② 跨包的父类缺省的方法也不能重写

  1. 子类方法抛出的异常不能大于父类被重写方法的异常

此外,子类与父类中同名同参数的方法必须同时声明为非static的(即为重写),或者同时声明为static的(不是重写)。因为static方法是属于类的,子类无法覆盖父类的方法。

3.3 小结:方法的重载与重写

方法的重载:方法名相同,形参列表不同。不看返回值类型。

方法的重写:见上面。

(1)同一个类中

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.atguigu.inherited.method;

public class TestOverload {
public int max(int a, int b){
return a > b ? a : b;
}
public double max(double a, double b){
return a > b ? a : b;
}
public int max(int a, int b,int c){
return max(max(a,b),c);
}
}

(2)父子类中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.atguigu.inherited.method;

public class TestOverloadOverride {
public static void main(String[] args) {
Son s = new Son();
s.method(1);//只有一个形式的method方法

Daughter d = new Daughter();
d.method(1);
d.method(1,2);//有两个形式的method方法
}
}

class Father{
public void method(int i){
System.out.println("Father.method");
}
}
class Son extends Father{
public void method(int i){//重写
System.out.println("Son.method");
}
}
class Daughter extends Father{
public void method(int i,int j){//重载
System.out.println("Daughter.method");
}
}

3.4 练习

练习1:如果现在父类的一个方法定义成private访问权限,在子类中将此方法声明为default访问权限,那么这样还叫重写吗? (NO)

练习2:修改继承内容的练习2中定义的类Kids,在Kids中重新定义employeed()方法,覆盖父类ManKind中定义的employeed()方法,输出“Kids should study and no job.”

4. 再谈封装性中的4种权限修饰

权限修饰符:public,protected,缺省,private

修饰符 本类 本包 其他包子类 其他包非子类
private × × ×
缺省 √(本包子类和非子类都可见) × ×
protected √(本包子类和非子类都可见) √(其他包仅限于子类中可见) ×
public

外部类:public和缺省

成员变量、成员方法等:public,protected,缺省,private

1、外部类要跨包使用必须是public,否则仅限于本包使用

(1)外部类的权限修饰符如果缺省,本包使用没问题

image-20211230093627763

(2)外部类的权限修饰符如果缺省,跨包使用有问题

image-20211230094236974

2、成员的权限修饰符问题

(1)本包下使用:成员的权限修饰符可以是public、protected、缺省

image-20211230095320646

(2)跨包下使用:要求严格

image-20211230095817784

(3)跨包使用时,如果类的权限修饰符缺省,成员权限修饰符>类的权限修饰符也没有意义

image-20211230100219840

5. 关键字:super

5.1 super的理解

在Java类中使用super来调用父类中的指定操作:

  • super可用于访问父类中定义的属性
  • super可用于调用父类中定义的成员方法
  • super可用于在子类构造器中调用父类的构造器

注意:

  • 尤其当子父类出现同名成员时,可以用super表明调用的是父类中的成员
  • super的追溯不仅限于直接父类[从下往上一直找]
  • super和this的用法相像,this代表本类对象的引用,super代表父类的内存空间的标识

5.2 super的使用场景

5.2.1 子类中调用父类被重写的方法

  • 如果子类没有重写父类的方法,只要权限修饰符允许,在子类中完全可以直接调用父类的方法;
  • 如果子类重写了父类的方法,在子类中需要通过super.才能调用父类被重写的方法,否则默认调用的子类重写的方法

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.atguigu.inherited.method;

public class Phone {
public void sendMessage(){
System.out.println("发短信");
}
public void call(){
System.out.println("打电话");
}
public void showNum(){
System.out.println("来电显示号码");
}
}

//smartphone:智能手机
public class SmartPhone extends Phone{
//重写父类的来电显示功能的方法
public void showNum(){
//来电显示姓名和图片功能
System.out.println("显示来电姓名");
System.out.println("显示头像");

//保留父类来电显示号码的功能
super.showNum();//此处必须加super.,否则就是无限递归,那么就会栈内存溢出
}
}

总结:

  • 方法前面没有super.和this.

    • 先从子类找匹配方法,如果没有,再从直接父类找,再没有,继续往上追溯
  • 方法前面有this.

    • 先从子类找匹配方法,如果没有,再从直接父类找,再没有,继续往上追溯
  • 方法前面有super.

    • 从当前子类的直接父类找,如果没有,继续往上追溯

5.2.2 子类中调用父类中同名的成员变量

  • 如果实例变量与局部变量重名,可以在实例变量前面加this.进行区别
  • 如果子类实例变量和父类实例变量重名,并且父类的该实例变量在子类仍然可见,在子类中要访问父类声明的实例变量需要在父类实例变量前加super.,否则默认访问的是子类自己声明的实例变量
  • 如果父子类实例变量没有重名,只要权限修饰符允许,在子类中完全可以直接访问父类中声明的实例变量,也可以用this.实例访问,也可以用super.实例变量访问

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Father{
int a = 10;
int b = 11;
}
class Son extends Father{
int a = 20;

public void test(){
//子类与父类的属性同名,子类对象中就有两个a
System.out.println("子类的a:" + a);//20 先找局部变量找,没有再从本类成员变量找
System.out.println("子类的a:" + this.a);//20 先从本类成员变量找
System.out.println("父类的a:" + super.a);//10 直接从父类成员变量找

//子类与父类的属性不同名,是同一个b
System.out.println("b = " + b);//11 先找局部变量找,没有再从本类成员变量找,没有再从父类找
System.out.println("b = " + this.b);//11 先从本类成员变量找,没有再从父类找
System.out.println("b = " + super.b);//11 直接从父类局部变量找
}

public void method(int a, int b){
//子类与父类的属性同名,子类对象中就有两个成员变量a,此时方法中还有一个局部变量a
System.out.println("局部变量的a:" + a);//30 先找局部变量
System.out.println("子类的a:" + this.a);//20 先从本类成员变量找
System.out.println("父类的a:" + super.a);//10 直接从父类成员变量找

System.out.println("b = " + b);//13 先找局部变量
System.out.println("b = " + this.b);//11 先从本类成员变量找
System.out.println("b = " + super.b);//11 直接从父类局部变量找
}
}
class Test{
public static void main(String[] args){
Son son = new Son();
son.test();
son.method(30,13);
}
}

总结:起点不同(就近原则)

  • 变量前面没有super.和this.

    • 在构造器、代码块、方法中如果出现使用某个变量,先查看是否是当前块声明的局部变量
    • 如果不是局部变量,先从当前执行代码的本类去找成员变量
    • 如果从当前执行代码的本类中没有找到,会往上找父类声明的成员变量(权限修饰符允许在子类中访问的)
  • 变量前面有this.

    • 通过this找成员变量时,先从当前执行代码的==本类去找成员变量==
    • 如果从当前执行代码的本类中没有找到,会往上找==父类声明的成员变量(==权限修饰符允许在子类中访问的)
  • 变量前面super.

    • 通过super找成员变量,直接从当前执行代码的直接父类去找成员变量(权限修饰符允许在子类中访问的)
    • 如果直接父类没有,就去父类的父类中找(权限修饰符允许在子类中访问的)

特别说明:应该避免子类声明和父类重名的成员变量

在阿里的开发规范等文档中都做出明确说明:

image-20211230110411580

5.2.3 子类构造器中调用父类构造器

① 子类继承父类时,不会继承父类的构造器。只能通过“super(形参列表)”的方式调用父类指定的构造器

② 规定:“super(形参列表)”,必须声明在构造器的首行。(类似于this也在首行)

③ 我们前面讲过,在构造器的首行可以使用”this(形参列表)”,调用本类中重载的构造器,
结合②,结论:在构造器的首行,“this(形参列表)” 和 “super(形参列表)”只能二选一

④ 如果在子类构造器的首行既没有显示调用”this(形参列表)”,也没有显式调用”super(形参列表)”, [先父类后子类]
​ 则子类此构造器默认调用”super()”,即调用父类中空参的构造器

⑤ 由③和④得到结论:子类的任何一个构造器中,要么会调用本类中重载的构造器,要么会调用父类的构造器。
只能是这两种情况之一。 —–写this就是掉自己 写super就是掉父类 不写就是父类的无参构造器!!!!!

⑥ 由⑤得到:一个类中声明有n个构造器,最多有n-1个构造器中使用了”this(形参列表)”,则剩下的那个一定使用”super(形参列表)”。
这里的最后一个可能是写了super,也可能没写用的默认的父类无参的

开发中常见错误:

如果子类构造器中既未显式调用父类或本类的构造器,且父类中又没有空参的构造器,则编译出错

情景举例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A{

}
class B extends A{

}

class Test{
public static void main(String[] args){
B b = new B();
//A类和B类都是默认有一个无参构造,B类的默认无参构造中还会默认调用A类的默认无参构造
//但是因为都是默认的,没有打印语句,看不出来
}
}

情景举例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A{
A(){
System.out.println("A类无参构造器");
}
}
class B extends A{

}
class Test{
public static void main(String[] args){
B b = new B();
//A类显示声明一个无参构造,
//B类默认有一个无参构造,
//B类的默认无参构造中会默认调用A类的无参构造
//可以看到会输出“A类无参构造器"
}
}

情景举例3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A{
A(){
System.out.println("A类无参构造器");
}
}
class B extends A{
B(){
System.out.println("B类无参构造器");
}
}
class Test{
public static void main(String[] args){
B b = new B();
//A类显示声明一个无参构造,
//B类显示声明一个无参构造,
//B类的无参构造中虽然没有写super(),但是仍然会默认调用A类的无参构造
//可以看到会输出“A类无参构造器"和"B类无参构造器")
}
}

情景举例4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class A{
A(){
System.out.println("A类无参构造器");
}
}
class B extends A{
B(){
super();
System.out.println("B类无参构造器");
}
}
class Test{
public static void main(String[] args){
B b = new B();
//A类显示声明一个无参构造,
//B类显示声明一个无参构造,
//B类的无参构造中明确写了super(),表示调用A类的无参构造
//可以看到会输出“A类无参构造器"和"B类无参构造器")
}
}

情景举例5:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A{
A(int a){
System.out.println("A类有参构造器");
}
}
class B extends A{
B(){
System.out.println("B类无参构造器");
}
}
class Test05{
public static void main(String[] args){
B b = new B();
//A类显示声明一个有参构造,没有写无参构造,那么A类就没有无参构造了
//B类显示声明一个无参构造,
//B类的无参构造没有写super(...),表示默认调用A类的无参构造
//编译报错,因为A类没有无参构造
}
}

image-20200227141228450

情景举例6:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class A{
A(int a){
System.out.println("A类有参构造器");
}
}
class B extends A{
B(){
super();
System.out.println("B类无参构造器");
}
}
class Test06{
public static void main(String[] args){
B b = new B();
//A类显示声明一个有参构造,没有写无参构造,那么A类就没有无参构造了
//B类显示声明一个无参构造,
//B类的无参构造明确写super(),表示调用A类的无参构造
//编译报错,因为A类没有无参构造
}
}

image-20200303183542807

情景举例7:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class A{
A(int a){
System.out.println("A类有参构造器");
}
}
class B extends A{
B(int a){
super(a);
System.out.println("B类有参构造器");
}
}
class Test07{
public static void main(String[] args){
B b = new B(10);
//A类显示声明一个有参构造,没有写无参构造,那么A类就没有无参构造了
//B类显示声明一个有参构造,
//B类的有参构造明确写super(a),表示调用A类的有参构造
//会打印“A类有参构造器"和"B类有参构造器"
}
}

情景举例8:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class A{
A(){
System.out.println("A类无参构造器");
}
A(int a){
System.out.println("A类有参构造器");
}
}
class B extends A{
B(){
super();//可以省略,调用父类的无参构造
System.out.println("B类无参构造器");
}
B(int a){
super(a);//调用父类有参构造
System.out.println("B类有参构造器");
}
}
class Test8{
public static void main(String[] args){
B b1 = new B();
B b2 = new B(10);
}
}

5.3 小结:this与super

1、this和super的意义

this:当前对象

  • 在构造器和非静态代码块中,表示正在new的对象
  • 在实例方法中,表示调用当前方法的对象

super:引用父类声明的成员

2、this和super的使用格式

  • this
    • this.成员变量:表示当前对象的某个成员变量,而不是局部变量
    • this.成员方法:表示当前对象的某个成员方法,完全可以省略this.
    • this()或this(实参列表):调用另一个构造器协助当前对象的实例化,只能在构造器首行,只会找本类的构造器,找不到就报错
  • super
    • super.成员变量:表示当前对象的某个成员变量,该成员变量在父类中声明的
    • super.成员方法:表示当前对象的某个成员方法,该成员方法在父类中声明的
    • super()或super(实参列表):调用父类的构造器协助当前对象的实例化,只能在构造器首行,只会找直接父类的对应构造器,找不到就报错

5.4 练习

练习1:修改方法重写的练习2中定义的类Kids中employeed()方法,在该方法中调用父类ManKind的employeed()方法,然后再输出“but Kids should study and no job.”

练习2:修改继承中的练习3中定义的Cylinder类,在Cylinder类中覆盖findArea()方法,计算圆柱的表面积。考虑:findVolume方法怎样做相应的修改?

在CylinderTest类中创建Cylinder类的对象,设置圆柱的底面半径和高,并输出圆柱的表面积和体积。

1
2
3
4
5
6
7
8
9
10
public double findVolume(){
return super.findArea()*length; //直接super调用父类的方法
}

@Override
public double findArea(){
//S表=2πr²+2πrh
//直接super调用父类的属性 因为是private就改成调用get方法
return 2*Math.PI*super.getRadius()*super.getRadius()+2*Math.PI*super.getRadius()*this.length;
}

附加题:在CylinderTest类中创建一个Circle类的对象,设置圆的半径,计算输出圆的面积。体会父类和子类成员的分别调用。

1
2
3
Circle circle=new Circle();
circle.setRadius(2);
System.out.println(circle.findArea()); //直接自己调用自己方法就行

练习3:

1、写一个名为Account的类模拟账户。该类的属性和方法如下图所示。该类包括的属性:账号id,余额balance,年利率annualInterestRate;包含的方法:访问器方法(getter和setter方法),返回月利率的方法getMonthlyInterest(),取款方法withdraw(),存款方法deposit()。

image-20220324003430464

写一个用户程序测试Account类。在用户程序中,创建一个账号为1122、余额为20000、年利率4.5%的Account对象。使用withdraw方法提款30000元,并打印余额。
再使用withdraw方法提款2500元,使用deposit方法存款3000元,然后打印余额和月利率。

提示:在提款方法withdraw中,需要判断用户余额是否能够满足提款数额的要求,如果不能,应给出提示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class Account {
private int id; //账号
private double balance; //余额
private double annuallnterestRate; //年利率

//三个private属性的set和get方法
public void setId(int id){
this.id=id;
}

public int getId(){
return this.id;
}

public void setBalance(double balance) {
this.balance = balance;
}

public double getBalance() {
return this.balance;
}

public void setAnnuallnterestRate(double annuallnterestRate) {
this.annuallnterestRate = annuallnterestRate;
}

public double getAnnuallnterestRate() {
return this.annuallnterestRate;
}

//构造器
public Account(int id,double balance,double annuallnterestRate){
this.id=id;
this.balance=balance;
this.annuallnterestRate=annuallnterestRate;
}

//返回月利率
public double getMonthlyInterest(){
return this.annuallnterestRate/12;
}

//取款
public void withdraw(double amount){
if(balance<amount){
System.out.println("余额不够!!!");
return; //一定要return 不然余额还是会扣除
}
//取款
balance-=amount;
}

//存款
public void deposit(double amount){
//存款
balance+=amount;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class AccountTest {
public static void main(String[] args) {
Account account=new Account(1122,20000,0.045);
//取款30000
account.withdraw(30000);
System.out.println("现在余额是:"+account.getBalance());
//取款2500
account.withdraw(2500);
//存款3000
account.deposit(3000);
System.out.println("现在余额是:"+account.getBalance());
System.out.println("现在是月利率是:"+account.getMonthlyInterest());
}
}

运行结果如图所示:

image-20231003170202016

2、创建Account类的一个子类CheckAccount代表可透支的账户,该账户中定义一个属性overdraft代表可透支限额。在CheckAccount类中重写withdraw方法,其算法如下:

1
2
3
4
5
6
7
8
如果(取款金额<账户余额),
可直接取款
如果(取款金额>账户余额),
计算需要透支的额度
判断可透支额overdraft是否足够支付本次透支需要,如果可以
将账户余额修改为0,冲减可透支金额
如果不可以
提示用户超过可透支额的限额

要求:写一个用户程序测试CheckAccount类。在用户程序中,创建一个账号为1122、余额为20000、年利率4.5%,可透支限额为5000元的CheckAccount对象。

使用withdraw方法提款5000元,并打印账户余额和可透支额。

再使用withdraw方法提款18000元,并打印账户余额和可透支额。

再使用withdraw方法提款3000元,并打印账户余额和可透支额。

提示:

(1)子类CheckAccount的构造方法需要将从父类继承的3个属性和子类自己的属性全部初始化。【不然被报错】

(2)父类Account的属性balance被设置为private,但在子类CheckAccount的withdraw方法中需要修改它的值,因此应修改父类的balance属性,定义其为protected。【也可以不修改,子类继承父类所有方法 直接用super.setBalance(super.getBalance()-amount);】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//代表 可透支的账户
public class CheckAccount extends Account { //子类
private double overdraft; // 可透支额度

//子类构造器
public CheckAccount(int id, double balance, double annuallnterestRate,double overdraft) {
super(id, balance, annuallnterestRate); //显式用super调用父类构造器 [如果不写的话就默认是super()无参构造器]
this.overdraft=overdraft; //自己属性
}

//get和set方法
public double getOverdraft() {
return overdraft;
}

public void setOverdraft(double overdraft) {
this.overdraft = overdraft;
}

//重写withdraw方法取款
@Override
public void withdraw(double amount){
//因为子类没有balance属性 所以不写super也会从子类找到父类
if(amount<super.getBalance()){
super.setBalance(super.getBalance()-amount); //符合条件可以取款
}
if(amount>super.getBalance()){
double cha=amount-super.getBalance(); //需要透支的额度
if(this.overdraft>cha){
super.setBalance(0); //账户余额修改为0 减少可透支余额
this.overdraft-=cha;
}else{
System.out.println("超过了可透支余额的限额");
return;
}
}

}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class CheckAccountTest {
public static void main(String[] args) {
CheckAccount checkAccount=new CheckAccount(1122,20000,0.045,5000);
System.out.println("一开始余额是:"+checkAccount.getOverdraft());
System.out.println("一开始可透支额度是:"+checkAccount.getBalance());
System.out.println("--------------------------------------------");
checkAccount.withdraw(5000);
System.out.println("取了5000之后余额是:"+checkAccount.getBalance());
System.out.println("现在可透支额度是:"+checkAccount.getOverdraft());
System.out.println("--------------------------------------------");
checkAccount.withdraw(18000);
System.out.println("取了18000之后余额是:"+checkAccount.getBalance());
System.out.println("现在可透支额度是:"+checkAccount.getOverdraft());
System.out.println("--------------------------------------------");
checkAccount.withdraw(3000);
System.out.println("取了3000之后余额是:"+checkAccount.getBalance());
System.out.println("现在可透支额度是:"+checkAccount.getOverdraft());

}
}

运行结果如下图所示:

image-20231003172602789

6. 子类对象实例化全过程

image-20220324003713230

1
Dog dog = new Dog("小花","小红");
image-20220324003735416 image-20220324003813163

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Creature {
public Creature() {
System.out.println("Creature无参数的构造器");
}
}
class Animal extends Creature {
public Animal(String name) {
System.out.println("Animal带一个参数的构造器,该动物的name为" + name);
}
public Animal(String name, int age) {
this(name); //调用上面的构造器
System.out.println("Animal带两个参数的构造器,其age为" + age);
}
}
public class Dog extends Animal {
public Dog() {
super("汪汪队阿奇", 3); //调用父类的构造器
System.out.println("Dog无参数的构造器");
}
public static void main(String[] args) {
new Dog();
}
}

运行结果如下图所示:

image-20231003201658599

7. 面向对象特征三:多态性

一千个读者眼中有一千个哈姆雷特。

7.1 多态的形式和体现

7.1.1 对象的多态性

多态性,是面向对象中最重要的概念,在Java中的体现:对象的多态性:父类的引用指向子类的对象

格式:(父类类型:指子类继承的父类类型,或者实现的接口类型)

1
父类类型 变量名 = 子类对象;

举例:

1
2
3
4
5
Person p = new Student();

Object o = new Person();//Object类型的变量o,指向Person类型的对象

o = new Student(); //Object类型的变量o,指向Student类型的对象

对象的多态:在Java中,子类的对象可以替代父类的对象使用。所以,一个引用类型变量可能指向(引用)多种不同类型的对象

7.1.2 多态的理解

Java引用变量有两个类型:编译时类型运行时类型。编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。
简称:编译时,看左边;运行时,看右边。

  • 若编译时类型和运行时类型不一致,就出现了对象的多态性(Polymorphism)

  • 多态情况下,“看左边”:看的是父类的引用(父类中不具备子类特有的方法)

    “看右边”:看的是子类的对象(实际运行的是子类重写父类的方法)
  • 多态的使用前提:① 类的继承关系 ② 方法的重写

  • 多态的适用性:适用于方法,不适用于属性

7.1.3 举例

创建人、男人和女人三个class。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Person {
String name;
int age;
int id=1001;

public void eat(){
System.out.println("人吃饭");
}

public void walk(){
System.out.println("人走路");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Man extends Person{

boolean isSmoking;
int id=1002;
@Override
public void eat(){
System.out.println("男人多吃肉,长肌肉");
}
@Override
public void walk(){
System.out.println("男人笔挺的走路");
}

public void earnMoney(){
System.out.println("男人挣钱养家");
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Woman extends Person{

boolean isBeauty;

@Override
public void eat(){
System.out.println("女人应该少吃,减肥");
}

@Override
public void walk(){
System.out.println("女人窈窕的走路");
}

public void goShopping(){
System.out.println("女人喜欢逛街...");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class PersonTest {
public static void main(String[] args) {
//1.多态性之前的场景
Person p1=new Person();

//2.多态性 {左右不一致}
Person p2=new Man();
//走的是man的方法
System.out.println(p2.id);

}
}

image-20231003220858899

7.2 为什么需要多态性(polymorphism)?

开发中,有时我们在设计一个数组、或一个成员变量、或一个方法的形参、返回值类型时,无法确定它具体的类型,只能确定它是某个系列的类型。
[在设计时候无法确定具体是什么?]

案例:

(1)声明一个Animal类,包含eat()和jump()方法

(2)声明一个Cat类,重写eat()和jump()方法,并且多一个catchMouse()方法

(3)声明一个Dog类,重写eat()和jump()方法,并且多一个watchDoor()方法

(4)声明一个AnimalTest类,包含adopt(Animal animal)方法,主要是收养动物【这时候不知道收养的是什么】 –> 考虑多态性

1
2
3
4
5
6
7
8
class Animal{
public void eat(){
System.out.println("动物进食");
}
public void jump(){
System.out.println("动物跳");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
class Cat extends Animal{
@Override
public void eat(){
System.out.println("猫吃鱼");
}
@Override
public void jump(){
System.out.println("猫跳~~");
}
public void catchMouse(){
System.out.println("猫抓老鼠");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
class Dog extends Animal{
@Override
public void eat(){
System.out.println("狗吃骨头");
}
@Override
public void jump(){
System.out.println("狗急跳墙");
}
public void watchDoor(){
System.out.println("狗能看家");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class AnimalTest {
public static void main(String[] args) {
AnimalTest test=new AnimalTest();
//多态性就可以直接更换new的对象就行 就不用写很多adopt方法了
test.adopt(new Dog()); //就可以执行时候考虑具体的动物对象!!!
test.adopt(new Cat()); //就可以执行时候考虑具体的动物对象!!!
}
//领养动物
public void adopt(Animal animal){ //声明一个动物 但是实际上传进来new可以是Dog 或者Cat
animal.eat();
animal.jump();
//在这个情况下就不能写animal.子类方法()!!!! 因为设计的时候只是知道是个动物
}
}

7.3 多态的好处和弊端

好处:变量引用的子类对象不同,执行的方法就不同,实现动态绑定【要什么传入什么】。代码编写更灵活、功能更强大,可维护性和扩展性更好了。

弊端:一个引用类型变量如果声明为父类的类型,但实际引用的是子类对象,那么该变量就不能再访问子类中添加的属性和方法【只能访问被重写的方法和父类的属性】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//多态的好处!!!
public class AnimalTest {
public static void main(String[] args) {
AnimalTest test=new AnimalTest();
//多态性就可以直接更换new的对象就行 就不用写很多adopt方法了
test.adopt(new Dog()); //就可以执行时候考虑具体的动物对象!!!
test.adopt(new Cat()); //就可以执行时候考虑具体的动物对象!!!
}
//领养动物
public void adopt(Animal animal){ //声明一个动物 但是实际上传进来new可以是Dog 或者Cat ------多态的好处!!!!!!!
animal.eat();
animal.jump();
//在这个情况下就不能写animal.子类方法()!!!! 因为设计的时候只是知道是个动物
}
}

//多态的弊端!!!
//[基于7.1.3举例的Person Woman Man[]
public class PersonTest1 {
public static void main(String[] args) {
//向下转型
Person p1=new Man();
//不能调用子类特有的属性和方法 ------多态的弊端!!!!!!!!
//p1.earnMoney();
//System.out.println(p1.isSmoking);
}
}
}

开发中:

方法形参:父类,是多态使用最多的场合。即使增加了新的子类,方法也无需改变,提高了扩展性,符合开闭原则。【你具体的类可以更新,毕竟我设计的时候用的是父类,这个不改就行】

【开闭原则OCP】

  • 对扩展开放,对修改关闭
  • 通俗解释:软件系统中的各种组件,如模块(Modules)、类(Classes)以及功能(Functions)等,应该在不修改现有代码的基础上,引入新功能

7.4 虚方法调用(Virtual Method Invocation)

在Java中虚方法是指在编译阶段不能确定方法的调用入口地址,在运行阶段才能确定的方法,即可能被重写的方法。

1
2
Person e = new Student();
e.getInfo(); //调用Student类的getInfo()方法

子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚方法,父类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的。

举例:

image-20220324234208997

前提:Person类中定义了welcome()方法,各个子类重写了welcome()。

image-20220324234214932

执行:多态的情况下,调用对象的welcome()方法,实际执行的是子类重写的方法。

拓展:

静态链接(或早起绑定):当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。那么调用这样的方法,就称为非虚方法调用。比如调用静态方法、私有方法、final方法、父类构造器、本类重载构造器等。

动态链接(或晚期绑定):如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。调用这样的方法,就称为虚方法调用。比如调用重写的方法(针对父类)、实现的方法(针对接口)。

7.5 成员变量没有多态性

  • 若子类重写了父类方法,就意味着子类里定义的方法彻底覆盖了父类里的同名方法,系统将不可能把父类里的方法转移到子类中。

  • 对于实例变量则不存在这样的现象,即使子类里定义了与父类完全相同的实例变量,这个实例变量依然不可能覆盖父类中定义的实例变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.atguigu.polymorphism.grammar;

public class TestVariable {
public static void main(String[] args) {
Base b = new Sub();
System.out.println(b.a);
System.out.println(((Sub)b).a);

Sub s = new Sub();
System.out.println(s.a);
System.out.println(((Base)s).a);
}
}
class Base{
int a = 1;
}
class Sub extends Base{
int a = 2;
}

7.6 向上转型与向下转型

首先,一个对象在new的时候创建是哪个类型的对象,它从头至尾都不会变。即这个对象的运行时类型,本质的类型用于不会变。但是,把这个对象赋值给不同类型的变量时,这些变量的编译时类型却不同。

7.6.1 为什么要类型转换

​ 多态的弊端,因为多态我们就不能调用子类特有的方法和属性。所以,想要调用子类特有的方法必须做类型转换,使得编译通过

image-20220324235337563
  • 向上转型:当左边的变量的类型(父类) > 右边对象/变量的类型(子类),我们就称为向上转型

    • 此时,编译时按照左边变量的类型处理,就只能调用父类中有的变量和方法,不能调用子类特有的变量和方法了
    • 但是,运行时,仍然是对象本身的类型,所以执行的方法是子类重写的方法体。
    • 此时,一定是安全的,而且也是自动完成的
  • 向下转型:当左边的变量的类型(子类)<右边对象/变量的编译时类型(父类),我们就称为向下转型

    • 此时,编译时按照左边变量的类型处理,就可以调用子类特有的变量和方法了
    • 但是,运行时,仍然是对象本身的类型
    • 不是所有通过编译的向下转型都是正确的,可能会发生ClassCastException,为了安全,可以通过isInstanceof关键字进行判断

7.6.2 如何向上或向下转型

向上转型:自动完成

向下转型:(子类类型)父类变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//基于7.1.3举例的Person Woman Man
public class PersonTest1 {
public static void main(String[] args) {
//向下转型
Person p1=new Man();
//不能调用子类特有的属性和方法
//p1.earnMoney();
//System.out.println(p1.isSmoking);

//向下转型【强转】
Man m1=(Man)p1; //p1和m1指向堆的同一个地址
m1.earnMoney();
System.out.println(m1.isSmoking);

//可能会出现类型转换异常【ClassCastException】
//Person p2=new Woman();
//Man m2=(Man)p2;
//m2.earnMoney();
// |
// |

}
}

image-20231003220521144

7.6.3 instanceof关键字

但是向下转型类似于数据类型转换一样,可能会存在并不是这个子类,但是你要转移成这个子类,就会出现ClassCastException!!!

image-20231003221329249

为了避免ClassCastException的发生,Java提供了 instanceof 关键字,给引用变量做类型的校验。如下代码格式:

1
2
//检验对象a是否是数据类型A的对象,返回值为boolean型
对象a instanceof 数据类型A
  • 说明:
    • 只要用instanceof判断返回true的,那么强转为该类型就一定是安全的,不会报ClassCastException异常。
    • 如果对象a属于类A的子类B,a instanceof A值也为true。
    • 要求对象a所属的类与类A必须是子类和父类的关系,否则编译错误。
    • 如果a instanceOf A 返回true,则:a instanceOf superA 返回也是true。[A是superA的子类]

代码:

1
2
3
4
5
Person p2=new Woman();
if(p2 instanceof Man){ //如果属于女人类就进强制转换
Man m2=(Man)p2;
m2.earnMoney();
}

7.7 练习

练习1:笔试&面试

题目1:继承成员变量和继承方法的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Base {
int count = 10;
public void display() {
System.out.println(this.count); //展示base类的count属性10
}
}

class Sub extends Base { //子类
int count = 20;
@Override
public void display() {
System.out.println(this.count); //展示Sub类的count属性20
}
}

public class FieldMethodTest {
public static void main(String[] args){
//创建一个子类对象
Sub s = new Sub();
System.out.println(s.count); // 输出20
s.display(); // 输出20

//多态
Base b = s;
System.out.println(b == s); // 因为两个指向同一个子类对象 所以输出true
System.out.println(b.count); //这时候还是父类的属性 输出10
b.display(); //输出20
}
}

题目2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//考查多态的笔试题目:
public class InterviewTest1 {
public static void main(String[] args) {
//多态
Base base = new Sub();
base.add(1, 2, 3); //调用被重写的子类方法 输出sub_1
}
}

class Base {
public void add(int a, int... arr) {
System.out.println("base");
}
}

class Sub extends Base {
@Override
public void add(int a, int[] arr) {
System.out.println("sub_1");
}
}

题目3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//getXxx()和setXxx()声明在哪个类中,内部操作的属性就是哪个类里的。
public class InterviewTest2 {
public static void main(String[] args) {
//普通创建父类对象f
Father f = new Father();
//普通创建子类对象s
Son s = new Son();
System.out.println(f.getInfo()); //输出父类属性 atguigu
System.out.println(s.getInfo()); //输出子类属性 尚硅谷
//输出子类的方法
s.test(); //输出 尚硅谷 atguigu
System.out.println("-----------------");
//更改子类的属性为大硅谷
s.setInfo("大硅谷");
System.out.println(f.getInfo()); //输出 atguigu
System.out.println(s.getInfo()); //输出 大硅谷
s.test(); //输出 大硅谷 atguigu
}
}

class Father {
private String info = "atguigu";

//私有属性的get和set方法
public void setInfo(String info) {
this.info = info;
}

public String getInfo() {
return info;
}
}

class Son extends Father{ //继承父类
private String info = "尚硅谷";
@Override
public void setInfo(String info) {
this.info = info;
}
@Override
public String getInfo() {
return info;
}

public void test() {
System.out.println(this.getInfo()); //调用子类的属性 尚硅谷
System.out.println(super.getInfo()); //调用父类的属性 atguigu
}
}

题目4:多态是编译时行为还是运行时行为?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import java.util.Random;
class Animal{ //父类
protected void eat() {
System.out.println("animal eat food");
}
}

class Cat extends Animal{ //子类继承父类
@Override
protected void eat() {
System.out.println("cat eat fish");
}
}

class Dog extends Animal{ //子类继承父类
@Override
public void eat() {
System.out.println("Dog eat bone");
}
}

class Sheep extends Animal{ //子类继承父类
@Override
public void eat() {
System.out.println("Sheep eat grass");
}
}

public class InterviewTest{
public static Animal getInstance(int key) { //返回值是一个动物父类
switch (key) {
case 0:
return new Cat ();
case 1:
return new Dog ();
default:
return new Sheep ();
}
}

public static void main(String[] args) {
int key = new Random().nextInt(3); //随机生成0-3中的随机数
System.out.println(key); //输出随机数
//多态
Animal animal = getInstance(key); //获取子类
animal.eat(); //是哪个子类就输出哪个子类的方法
}
}

练习2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Person {
//protected修饰符 --可以跨包但是仅限子类可以获取到
protected String name="宋亚翔";
protected int age=24;
public String getInfo() {
return "Name: "+ name + "\n" +"age: "+ age;
}
}
class Student extends Person {
//protected修饰符 --可以跨包但是仅限子类可以获取到
protected String school="东北林业大学"; //子类特有的属性
@Override
public String getInfo() {
return "Name: "+ name + "\nage: "+ age + "\nschool: "+ school;
}
}
class Graduate extends Student{
public String major="计算机"; //子类特有的属性
@Override
public String getInfo()
{
return "Name: "+ name + "\nage: "+ age + "\nschool: "+ school+"\nmajor:"+major;
}
}

建立InstanceTest 类,在类中定义方法method(Person e);
在method中:
(1)根据e的类型调用相应类的getInfo()方法。
(2)根据e的类型执行:
如果e为Person类的对象,输出:
“a person”;
如果e为Student类的对象,输出:
“a student”
“a person ”
如果e为Graduate类的对象,输出:
“a graduated student”
“a student”
“a person”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class InstanceTest {
public static void main(String[] args) {
Person person=new Person();
Person person1=new Student();
Person person2=new Graduate();
InstanceTest test=new InstanceTest();
test.method(person);
System.out.println("--------------------------");
test.method(person1);
System.out.println("--------------------------");
test.method(person2);
System.out.println("--------------------------");
}

public void method(Person e){
//设置key来接到底是哪个类
int key = 0;
if(e instanceof Person){
key=0;
}
if(e instanceof Student){
key=1;
}
if(e instanceof Graduate){
key=2;
}
//根据key判断是哪个类型按照要求输出
switch (key){
case 0:System.out.println(e.getInfo());System.out.println("a person");
break;
case 1:System.out.println(e.getInfo());
System.out.println("a person");
System.out.println("a student");
break;
case 2:System.out.println(e.getInfo());
System.out.println("a person");
System.out.println("a student");
System.out.println("a graduate");
break;
}
}
}

image-20231005215343643

练习3:定义三个类,父类GeometricObject代表几何形状,子类Circle代表圆形,MyRectangle代表矩形。定义一个测试类GeometricTest,编写equalsArea方法测试两个对象的面积是否相等(注意方法的参数类型,利用动态绑定技术),编写displayGeometricObject方法显示对象的面积(注意方法的参数类型,利用动态绑定技术)。

image-20220325000034619
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//几何类
public class GeometricObject {
// 类图中#表示protected修饰符 --可以跨包但是只能是子类使用
protected String color;
protected double weight;

//有参构造器
protected GeometricObject(String color,double weight){
this.color=color;
this.weight=weight;
}

//set和get方法
public String getColor() {
return color;
}

public void setColor(String color) {
this.color = color;
}

public double getWeight() {
return weight;
}

public void setWeight(double weight) {
this.weight = weight;
}

//要被重写的方法
public double findArea(){
return 0.0; //默认输出0 后来要被重写
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//圆类
public class Circle extends GeometricObject{

//子类私有属性 -半径
private double radius;

//构造器
protected Circle(double radius,String color, double weight) {
super(color, weight); //【首行】调用父类构造器
this.radius=radius;
}

//set和get方法
public double getRadius() {
return radius;
}

public void setRadius(double radius) {
this.radius = radius;
}

@Override
public double findArea(){
return Math.PI*radius*radius; //重写方法 返回Πr平方²
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//矩形
public class MyRectangle extends GeometricObject{

private double width;
private double height;

//构造器
public MyRectangle(double width,double height,String color,double weight){
super(color, weight); //【首行】调用父类构造器
this.width=width;
this.height=height;
}

//set和get方法
public double getWidth() {
return width;
}

public void setWidth(double width) {
this.width = width;
}

public double getHeight() {
return height;
}

public void setHeight(double height) {
this.height = height;
}

@Override
public double findArea(){
return width*height; //重写方法 返回长*宽
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class GeometricTest {
public static void main(String[] args) {
//两个多态
GeometricObject g1=new Circle(3,"黄色",10);
GeometricObject g2=new MyRectangle(3,4,"绿色",10);
//设定test对象测试方法
GeometricTest test=new GeometricTest();
System.out.println(test.equalsArea(g1,g2));
System.out.println(test.displayGeometricObject(g1));
System.out.println(test.displayGeometricObject(g2));
}

public boolean equalsArea(GeometricObject g1,GeometricObject g2){
return g1.findArea()==g2.findArea(); //判断两个对象的面积是否相等
}

public double displayGeometricObject(GeometricObject g){
//根据对象输出对应的方法
return g.findArea();
}

}

结果截图如下:

image-20231005222429951

8. Object 类

8.1 如何理解根父类

java.lang.Object是类层次结构的根类,即所有其它类的父类。每个类都使用 Object 作为超类。

image-20220503104750655
  • Object类型的变量与除Object以外的任意引用数据类型的对象都存在多态引用

    1
    2
    3
    4
    method(Object obj){…} //可以接收任何类作为其参数

    Person o = new Person();
    method(o);
  • 所有对象(包括数组)都实现这个类的方法。

  • 如果一个类没有特别指定父类,那么默认则继承自Object类。例如:

    1
    2
    3
    4
    5
    6
    7
    public class Person {
    ...
    }
    //等价于:
    public class Person extends Object {
    ...
    }

8.2 Object类的方法

根据JDK源代码及Object类的API文档,Object类当中包含的方法有11个。这里我们主要关注其中的6个:

1、(重点)equals()

= =:

  • 1.基本类型:比较值:只要两个变量的值相等,即为true。

    1
    2
    int a=5; 
    if(a==6){…}
  • 2.引用类型:比较引用(是否指向同一个对象):只有指向同一个对象时,==才返回true。

    1
    2
    3
    Person p1=new Person();  	    
    Person p2=new Person();
    if (p1==p2){…}
    • 用“==”进行比较时,符号两边的数据类型必须兼容(可自动转换的基本数据类型除外),否则编译出错

equals():所有类都继承了Object,也就获得了equals()方法。还可以重写。

  • 只能比较引用类型,Object类源码中equals()的作用与“==”相同:比较是否指向同一个对象。

    image-20220503104750655
  • 格式:obj1.equals(obj2)

  • 没有重写Object中equals()方法[自定义的类] –> 比较两个对象的引用地址是否相同(或比较两个对象是否指向了堆空间中的同一个对象实体)

    重写了Object类中的equals()方法[String、File、Date和包装类等] –> 用于比较两个对象的实体内容是否相等

  • 重写equals()方法的原则

    • 对称性:如果x.equals(y)返回是“true”,那么y.equals(x)也应该返回是“true”。

    • 自反性:x.equals(x)必须返回是“true”。

    • 传递性:如果x.equals(y)返回是“true”,而且y.equals(z)返回是“true”,那么z.equals(x)也应该返回是“true”。

    • 一致性:如果x.equals(y)返回是“true”,只要x和y内容一直不变,不管你重复x.equals(y)多少次,返回都是“true”。

    • 任何情况下,x.equals(null),永远返回是“false”;

      ​ x.equals(和x不同类型的对象)永远返回是“false”。

  • 重写equals()方法的方法

    1
    2
    // 1.自己写 
    // 2.idea自动创建[Alt+Ins] 选中equals和hashCode()
  • 重写举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class User{
String name;
int age;

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

//重写equals()
// 1.自己写
// 2.idea自动创建[Alt+Ins] 选中equals和hashCode()
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User user)) return false;
return age == user.age && Objects.equals(name, user.name);
}

}

面试题:==和equals的区别

从我面试的反馈,85%的求职者“理直气壮”的回答错误…

  • == 既可以比较基本类型也可以比较引用类型。对于基本类型就是比较值,对于引用类型就是比较内存地址

  • equals的话,它是属于java.lang.Object类里面的方法,如果该方法没有被重写过默认也是==;我们可以看到String等类的equals方法是被重写过的,而且String类在日常开发中用的比较多,久而久之,形成了equals是比较值的错误观点。

  • 具体要看自定义类里有没有重写Object的equals方法来判断。

  • 通常情况下,重写equals方法,会比较类中的相应属性是否都相等。

练习1:

1
2
3
4
5
6
7
8
9
10
11
12
int it = 65;
float fl = 65.0f;
System.out.println("65和65.0f是否相等?" + (it == fl)); // 两个是基本数据类型比较值 输出true 【int会自动类型转换为float】

char ch1 = 'A'; char ch2 = 12;
System.out.println("65和'A'是否相等?" + (it == ch1)); // 两个是基本数据类型比较值 输出true 【char会自动类型转换为int比较】
System.out.println("12和ch2是否相等?" + (12 == ch2)); // 两个是基本数据类型比较值 输出true 【char会自动类型转换为int比较】

String str1 = new String("hello");
String str2 = new String("hello");
System.out.println("str1和str2是否相等?"+ (str1 == str2)); // 两个是引用数据类型比较是不是同一对象 输出false
System.out.println("str1是否equals str2?"+(str1.equals(str2))); // 两个是引用数据类型比较是不是同一对象 又因为是String类型重写了方法比较值 输出true

练习2:

编写Order类,有int型的orderId,String型的orderName,相应的getter()和setter()方法,两个参数的构造器,重写父类的equals()方法:public boolean equals(Object obj),并判断测试类中创建的两个对象是否相等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import java.util.Objects;
public class Order {
//因为要设置get和set方法 所以需要设置为private
private int orderld;
private String orderName;

//get和set方法
public int getOrderld() {
return orderld;
}

public void setOrderld(int orderld) {
this.orderld = orderld;
}

public String getOrderName() {
return orderName;
}

public void setOrderName(String orderName) {
this.orderName = orderName;
}

//两个参数的构造器
public Order(int orderld,String ordername){
this.orderld=orderld;
this.orderName=ordername;
}

//根据idea重写 Alt+ins
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Order order)) return false;
return getOrderld() == order.getOrderld() && Objects.equals(getOrderName(), order.getOrderName());
}

}
1
2
3
4
5
6
7
8
9
10
11
public class OrderTest {
public static void main(String[] args) {
Order order1=new Order(1,"宋亚翔");
Order order2=new Order(1,"宋亚翔");
Order order3=new Order(2,"宋亚翔");
Order order4=new Order(1,"李武");
System.out.println(order1.equals(order2)); //必须是两个属性都相同才行
System.out.println(order1.equals(order3));
System.out.println(order1.equals(order4));
}
}

代码结果如下:

image-20231006111943546

练习3:

请根据以下代码自行定义能满足需要的MyDate类,在MyDate类中覆盖equals方法,使其判断当两个MyDate类型对象的年月日都相同时,结果为true,否则为false。 public boolean equals(Object o)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class EqualsTest {
public static void main(String[] args) {
MyDate m1 = new MyDate(14, 3, 1976);
MyDate m2 = new MyDate(14, 3, 1976);
if (m1 == m2) {
System.out.println("m1==m2");
} else {
System.out.println("m1!=m2"); // m1 != m2
}

if (m1.equals(m2)) {
System.out.println("m1 is equal to m2");// m1 is equal to m2
} else {
System.out.println("m1 is not equal to m2");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import java.util.Objects;
public class MyDate {
//三个参数设置为private属性 加上三个get和set方法
private int year;
private int month;
private int day;

//get和set方法
public int getYear() {
return year;
}

public void setYear(int year) {
this.year = year;
}

public int getMonth() {
return month;
}

public void setMonth(int month) {
this.month = month;
}

public int getDay() {
return day;
}

public void setDay(int day) {
this.day = day;
}

//构造器 因为Test里面是有参的
public MyDate(int day, int month, int year) {
this.year = year;
this.month = month;
this.day = day;
}

//重写equals方法
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof MyDate myDate)) return false;
return getYear() == myDate.getYear() && getMonth() == myDate.getMonth() && getDay() == myDate.getDay();
}

}

2、(重点)toString()

方法签名:public String toString()

① 默认情况下,toString()返回的是“对象的运行时类型 @ 对象的hashCode值的十六进制形式”

② 在进行String与其它类型数据的连接操作时,自动调用toString()方法

1
2
3
Date now=new Date();
System.out.println(“now=”+now); //相当于
System.out.println(“now=”+now.toString());

③ 如果我们直接System.out.println(对象),默认会自动调用这个对象的toString()

因为Java的引用数据类型的变量中存储的实际上时对象的内存地址,但是Java对程序员隐藏内存地址信息,所以不能直接将内存地址显示出来,所以当你打印对象时,JVM帮你调用了对象的toString()。

④ 可以根据需要在用户自定义类型中重写toString()方法
如String 类重写了toString()方法,返回字符串的值。

1
2
s1="hello";
System.out.println(s1);//相当于System.out.println(s1.toString());

例如自定义的Person类:

1
2
3
4
5
6
7
8
9
public class Person {  
private String name;
private int age;

@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
}
}

练习:定义两个类,父类GeometricObject代表几何形状,子类Circle代表圆形。

image-20220325002959156 image-20220325002932102
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class GeometricObject {
protected String color;
protected double weight;

//get和set方法
public String getColor() {
return color;
}

public void setColor(String color) {
this.color = color;
}

public double getWeight() {
return weight;
}

public void setWeight(double weight) {
this.weight = weight;
}

//空参构造器
protected GeometricObject(){
this.weight=1.0;
this.color="white"; //初始化对象
}

//有参构造器
protected GeometricObject(String color,double weight){
this.color=color;
this.weight=weight;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import java.util.Objects;
public class Circle extends GeometricObject {
private double radius;

public Circle(){
this.radius=1.0;
super.color="white"; //初始化三个属性
super.weight=1.0;
}

public Circle(double radius){
this.radius=radius; //radius根据参数构造器确定
super.color="white";
super.weight=1.0;
}

public Circle(double radius,String color,double weight){
super(color,weight); //显式调用父类有参构造器
this.radius=radius;
}

//set和get方法
public double getRaidus() {
return radius;
}

public void setRaidus(double raidus) {
this.radius = raidus;
}

//计算圆面积
public double findArea(){
return Math.PI*radius*radius; // π×r²
}

@Override
public String toString() { //重写toString()方法
return "Circle{" +
"radius=" + radius +
", color='" + color + '\'' +
", weight=" + weight +
'}';
}

@Override
public boolean equals(Object o) { //重写equals()方法
if (this == o) return true;
if (!(o instanceof Circle circle)) return false;
return Double.compare(radius, circle.radius) == 0;
}

}

3、clone()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//Object类的clone()的使用
public class CloneTest {
public static void main(String[] args) {
Animal a1 = new Animal("花花");
try {
Animal a2 = (Animal) a1.clone();
System.out.println("原始对象:" + a1);
a2.setName("毛毛");
System.out.println("clone之后的对象:" + a2);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}

class Animal implements Cloneable{
private String name;

public Animal() {
super();
}

public Animal(String name) {
super();
this.name = name;
}

public String getName() {
return name;
}

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

@Override
public String toString() {
return "Animal [name=" + name + "]";
}

@Override
protected Object clone() throws CloneNotSupportedException {
// TODO Auto-generated method stub
return super.clone();
}

}

4、finalize()

  • 当对象被回收时,系统自动调用该对象的 finalize() 方法。(不是垃圾回收器调用的,是本类对象调用的)
    • 永远不要主动调用某个对象的finalize方法,应该交给垃圾回收机制调用。
  • 什么时候被回收:当某个对象没有任何引用时,JVM就认为这个对象是垃圾对象,就会在之后不确定的时间使用垃圾回收机制来销毁该对象,在销毁该对象前,会先调用 finalize()方法。
  • 子类可以重写该方法,目的是在对象被清理之前执行必要的清理操作。比如,在方法内断开相关连接资源。
    • 如果重写该方法,让一个新的引用变量重新引用该对象,则会重新激活对象。
  • 在JDK 9中此方法已经被标记为过时的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class FinalizeTest {
public static void main(String[] args) {
Person p = new Person("Peter", 12);
System.out.println(p);
p = null;//此时对象实体就是垃圾对象,等待被回收。但时间不确定。
System.gc();//强制性释放空间
}
}

class Person{
private String name;
private int age;

public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
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;
}
//子类重写此方法,可在释放对象前进行某些操作
@Override
protected void finalize() throws Throwable {
System.out.println("对象被释放--->" + this);
}
@Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}

}

5、getClass()

public final Class<?> getClass():获取对象的运行时类型

因为Java有多态现象,所以一个引用数据类型的变量的编译时类型与运行时类型可能不一致,因此如果需要查看这个变量实际指向的对象的类型,需要用getClass()方法

1
2
3
4
public static void main(String[] args) {
Object obj = new Person();
System.out.println(obj.getClass());//运行时类型
}

结果:

1
class com.atguigu.java.Person

6、hashCode()

public int hashCode():返回每个对象的hash值。(后续在集合框架章节重点讲解)

1
2
3
4
public static void main(String[] args) {
System.out.println("AA".hashCode());//2080
System.out.println("BB".hashCode());//2112
}

8.3 native关键字的理解

使用native关键字说明这个方法是原生函数,也就是这个方法是用C/C++等实现的,并且被编译成了DLL,由Java去调用。

  • 本地方法是有方法体的,用c语言编写。由于本地方法的方法体源码没有对我们开源,所以我们看不到方法体

  • 在Java中定义一个native方法时,并不提供实现体。

1. 为什么要用native方法

Java使用起来非常方便,然而有些层次的任务用java实现起来不容易,或者我们对程序的效率很在意时,例如:Java需要与一些底层操作系统或某些硬件交换信息时的情况。native方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解Java应用之外的繁琐的细节。

2. native声明的方法,对于调用者,可以当做和其他Java方法一样使用

native method的存在并不会对其他类调用这些本地方法产生任何影响,实际上调用这些方法的其他类甚至不知道它所调用的是一个本地方法。JVM将控制调用本地方法的所有细节。

第08章_面向对象编程(高级)


本章专题与脉络

第2阶段:Java面向对象编程-第08章

1. 关键字:static

回顾类中的实例变量(即非static的成员变量)

1
2
3
4
5
6
7
8
9
10
11
12
class Circle{
//私有属性radius
private double radius;
//有参构造器
public Circle(double radius){
this.radius=radius;
}
//findArea()方法
public double findArea(){
return Math.PI*radius*radius;
}
}

创建两个Circle对象:

1
2
Circle c1=new Circle(2.0);	//c1.radius=2.0
Circle c2=new Circle(3.0); //c2.radius=3.0

Circle类中的变量radius是一个实例变量(instance variable),它属于类的每一个对象,c1中的radius变化不会影响c2的radius。

–> 如果想让一个成员变量被类的所有实例所共享,就用static修饰即可,称为类变量/类属性

1.1 类属性、类方法的设计思想

当我们编写一个类时,其实就是在描述其对象的属性和行为,而并没有产生实质上的对象,只有通过new关键字才会产出对象,这时系统才会分配内存空间给对象,其方法才可以供外部调用。我们有时候希望无论是否产生了对象或无论产生了多少对象的情况下,某些特定的数据在内存空间里只有一份。例如,所有的中国人都有个国家名称,每一个中国人都共享这个国家名称,不必在每一个中国人的实例对象中都单独分配一个用于代表国家名称的变量。

image-20220325213629311

此外,在类中声明的实例方法,在类的外面必须要先创建对象,才能调用。但是有些方法的调用者和当前类的对象无关,这样的方法通常被声明为类方法,由于不需要创建对象就可以调用类方法,从而简化了方法的调用。

这里的类变量、类方法,只需要使用static修饰即可。所以也称为静态变量、静态方法。

1.2 static关键字

  • 使用范围:

    • 在Java类中,可用static修饰属性、方法、代码块、内部类
  • 被修饰后的成员具备以下特点:

    • 随着类的加载而加载
    • 优先于对象存在
    • 修饰的成员,被所有对象所共享
    • 访问权限允许时,可不创建对象,直接被类调用

1.3 静态变量

1.3.1 语法格式

使用static修饰的成员变量就是静态变量(或类变量、类属性)

1
2
3
[修饰符] class{
[其他修饰符] static 数据类型 变量名;
}

1.3.2 静态变量的特点

  • 静态变量的默认值规则和实例变量一样。

  • 静态变量值是所有对象共享。

  • 静态变量在本类中,可以在任意方法、代码块、构造器中直接使用。

  • 如果权限修饰符允许,在其他类中可以通过“类名.静态变量”直接访问,也可以通过“对象.静态变量”的方式访问(但是更推荐使用类名.静态变量的方式)。

  • 静态变量的get/set方法也静态的,当局部变量与静态变量重名时,使用“类名.静态变量”进行区分。

1.3.3 举例

举例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Chinese{
//实例变量
String name;
int age;
//类变量
static String nation;//国籍

public Chinese() {
}

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

@Override
public String toString() {
return "Chinese{" +
"name='" + name + '\'' +
", age=" + age +
", nation='" + nation + '\'' +
'}';
}
}
public class StaticTest {
public static void main(String[] args) {
Chinese c1 = new Chinese("康师傅",36);
c1.nation = "中华人民共和国";

Chinese c2 = new Chinese("老干妈",66);

System.out.println(c1);
System.out.println(c2);

System.out.println(Chinese.nation);
}
}

对应的内存结构:(以经典的JDK6内存解析为例,此时静态变量存储在方法区)

image-20220514183814514

举例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.atguigu.keyword;

public class Employee {
private static int total;//这里私有化,在类的外面必须使用get/set方法的方式来访问静态变量
static String company; //这里缺省权限修饰符,是为了方便类外以“类名.静态变量”的方式访问
private int id;
private String name;

public Employee() {
total++;
id = total;//这里使用total静态变量的值为id属性赋值
}

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

public void setId(int id) {
this.id = id;
}

public int getId() {
return id;
}

public String getName() {
return name;
}

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

public static int getTotal() {
return total;
}

public static void setTotal(int total) {
Employee.total = total;
}

@Override
public String toString() {
return "Employee{company = " + company + ",id = " + id + " ,name=" + name +"}";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.atguigu.keyword;

public class TestStaticVariable {
public static void main(String[] args) {
//静态变量total的默认值是0
System.out.println("Employee.total = " + Employee.getTotal());

Employee e1 = new Employee("张三");
Employee e2 = new Employee("李四");
System.out.println(e1);//静态变量company的默认值是null
System.out.println(e2);//静态变量company的默认值是null
System.out.println("Employee.total = " + Employee.getTotal());//静态变量total值是2

Employee.company = "尚硅谷";
System.out.println(e1);//静态变量company的值是尚硅谷
System.out.println(e2);//静态变量company的值是尚硅谷

//只要权限修饰符允许,虽然不推荐,但是也可以通过“对象.静态变量”的形式来访问
e1.company = "超级尚硅谷";

System.out.println(e1);//静态变量company的值是超级尚硅谷
System.out.println(e2);//静态变量company的值是超级尚硅谷
}
}

1.3.4 内存解析

image-20220104100145059

1.4 静态方法

1.4.1 语法格式

用static修饰的成员方法就是静态方法。

1
2
3
4
5
[修饰符] class{
[其他修饰符] static 返回值类型 方法名(形参列表){
方法体
}
}

1.4.2 静态方法的特点

  • 静态方法在本类的任意方法、代码块、构造器中都可以直接被调用。
  • 只要权限修饰符允许,静态方法在其他类中可以通过“类名.静态方法“的方式调用。也可以通过”对象.静态方法“的方式调用(但是更推荐使用类名.静态方法的方式)。
  • 在static方法内部只能访问类的static修饰的属性或方法,不能访问类的非static的结构。
  • 静态方法可以被子类继承,但不能被子类重写。
  • 静态方法的调用都只看编译时类型。
  • 因为不需要实例就可以访问static方法,因此static方法内部不能有this,也不能有super。如果有重名问题,使用“类名.”进行区别。

1.4.3 举例

1
2
3
4
5
6
7
8
9
10
11
package com.atguigu.keyword;

public class Father {
public static void method(){
System.out.println("Father.method");
}

public static void fun(){
System.out.println("Father.fun");
}
}
1
2
3
4
5
6
7
8
package com.atguigu.keyword;

public class Son extends Father{
// @Override //尝试重写静态方法,加上@Override编译报错,去掉Override不报错,但是也不是重写
public static void fun(){
System.out.println("Son.fun");
}
}
1
2
3
4
5
6
7
8
9
10
11
package com.atguigu.keyword;

public class TestStaticMethod {
public static void main(String[] args) {
Father.method();
Son.method();//继承静态方法

Father f = new Son();
f.method();//执行Father类中的method
}
}

1.5 练习

笔试题:如下程序执行会不会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class StaticTest {
//代码入口
public static void main(String[] args) { //静态的方法只能调用静态的方法和属性
Demo test = null; //创建对象
test.hello(); //对象.静态方法
Demo.hello(); //类.静态方法
}
}

class Demo{
//静态方法
public static void hello(){
System.out.println("hello!");
}
}
最终输出:
hello!
hello!

练习:

编写一个类实现银行账户的概念,包含的属性有“帐号”、“密码”、“存款余额”、“利率”、“最小余额”,定义封装这些属性的方法。账号要自动生成。

编写主类,使用银行账户类,输入、输出3个储户的上述信息。

考虑:哪些属性可以设计成static属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package staticTest;

public class Account {

private int id; //账号

private String password;//密码

private double balance; //余额

private static double interestRate;//利率

private static double minBalance = 1.0;//最小余额

private static int init = 1001;//用于自动生成id的基数

//无参构造器
public Account() {
this.id = init;
init++;
password = "000000"; //初始化密码
}

//有参构造器
public Account(String password, double balance) {
this.password = password;
this.balance = balance;
this.id = init;
init++;
}

//set和get方法
public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public double getBalance() {
return balance;
}

public void setBalance(double balance) {
this.balance = balance;
}

public static double getInterestRate() {
return interestRate;
}

public static void setInterestRate(double interestRate) {
Account.interestRate = interestRate;
}

public static double getMinBalance() {
return minBalance;
}

public static void setMinBalance(double minBalance) {
Account.minBalance = minBalance;
}

//重写toString方法
@Override
public String toString() {
return "Account{" +
"id=" + id +
", password='" + password + '\'' +
", balance=" + balance +
'}';
}
}

2. 单例(Singleton)设计模式

2.1 设计模式概述

设计模式是在大量的实践中总结理论化之后优选的代码结构、编程风格、以及解决问题的思考方式。设计模式免去我们自己再思考和摸索。就像是经典的棋谱,不同的棋局,我们用不同的棋谱。”套路”

经典的设计模式共有23种。每个设计模式均是特定环境下特定问题的处理方法。

image-20220520174508815

简单工厂模式并不是23中经典模式的一种,是其中工厂方法模式的简化版

对软件设计模式的研究造就了一本可能是面向对象设计方面最有影响的书籍:《设计模式》:《Design Patterns: Elements of Reusable Object-Oriented Software》(即后述《设计模式》一书),由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著(Addison-Wesley,1995)。这几位作者常被称为”四人组(Gang of Four)”,而这本书也就被称为”四人组(或 GoF)”书。

2.2 何为单例模式

所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。

2.3 实现思路

如果我们要让类在一个虚拟机中只能产生一个对象,我们首先必须将类的构造器的访问权限设置为private,[就不能用new操作符在类的外部产生类的对象],但在类内部仍可以产生该类的对象。因为在类的外部开始还无法得到类的对象,只能调用该类的某个静态方法以返回类内部创建的对象,静态方法只能访问类中的静态成员变量,所以,指向类内部产生的该类对象的变量也必须定义成静态的

2.4 单例模式的两种实现方式

2.4.1 饿汉式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Singleton {
// 1.私有化构造器
private Singleton() {
}

// 2.内部提供一个当前类的实例
// 4.此实例也必须静态化
private static Singleton single = new Singleton();

// 3.提供公共的静态的方法,返回当前类的对象
public static Singleton getInstance() {
return single;
}
}

2.4.2 懒汉式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Singleton {
// 1.私有化构造器
private Singleton() {
}
// 2.内部提供一个当前类的实例
// 4.此实例也必须静态化
private static Singleton single;
// 3.提供公共的静态的方法,返回当前类的对象
public static Singleton getInstance() {
if(single == null) {
single = new Singleton();
}
return single;
}
}

2.4.3 饿汉式 vs 懒汉式

饿汉式:

  • 特点:立即加载,即在使用类的时候已经将对象创建完毕。
  • 优点:实现起来简单;没有多线程安全问题。
  • 缺点:当类被加载的时候,会初始化static的实例,静态变量被创建并分配内存空间,从这以后,这个static的实例便一直占着这块内存,直到类被卸载时,静态变量被摧毁,并释放所占有的内存。因此在某些特定条件下会耗费内存

懒汉式:

  • 特点:延迟加载,即在调用静态方法时实例才被创建。
  • 优点:实现起来比较简单;当类被加载的时候,static的实例未被创建并分配内存空间,当静态方法第一次被调用时,初始化实例变量,并分配内存,因此在某些特定条件下会节约内存
  • 缺点:在多线程环境中,这种实现方法是完全错误的,线程不安全,根本不能保证单例的唯一性。
    • 说明:在多线程章节,会将懒汉式改造成线程安全的模式。

2.5 单例模式的优点及应用场景

由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。

举例:

image-20220325222541203

应用场景

  • Windows的Task Manager (任务管理器)就是很典型的单例模式

  • Windows的Recycle Bin (回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。

  • Application 也是单例的典型应用

  • 应用程序的日志应用,一般都使用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只

    能有一个实例去操作,否则内容不好追加。

  • 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。

3. 理解main方法的语法

由于JVM需要调用类的main()方法,所以该方法的访问权限必须是public,又因为JVM在执行main()方法时不必创建对象,所以该方法必须是static的,该方法接收一个String类型的数组参数,该数组中保存执行Java命令时传递给所运行的类的参数。

又因为main() 方法是静态的,我们不能直接访问该类中的非静态成员,必须创建该类的一个实例对象后,才能通过这个对象去访问类中的非静态成员,这种情况,我们在之前的例子中多次碰到。

命令行参数用法举例

1
2
3
4
5
6
7
public class CommandPara {
public static void main(String[] args) {
for (int i = 0; i < args.length; i++) {
System.out.println("args[" + i + "] = " + args[i]);
}
}
}
1
2
//运行程序CommandPara.java
java CommandPara "Tom" "Jerry" "Shkstart"
1
2
3
4
//输出结果
args[0] = Tom
args[1] = Jerry
args[2] = Shkstart

image-20220325223215924

IDEA工具:

(1)配置运行参数

image-20211228101828718 image-20211228102022216

(2)运行程序

image-20211228102059327

笔试题:

1
2
3
4
5
6
7
8
9
//此处,Something类的文件名叫OtherThing.java
class Something {
public static void main(String[] something_to_do) {
System.out.println("Do something ...");
}
}


//上述程序是否可以正常编译、运行? 如果改一下类名的话,其他事可以啊,没什么影响。

4. 类的成员之四:代码块

如果成员变量想要初始化的值不是一个硬编码的常量值,而是需要通过复杂的计算或读取文件、或读取运行环境信息等方式才能获取的一些值,该怎么办呢?此时,可以考虑代码块/初始化块

  • 代码块(或初始化块)的作用

  • 对Java的类/对象进行初始化

  • 代码块(或初始化块)的分类

    • 静态代码块(static block): 用static修饰[只能被static修饰]

    • 非静态代码块:没有用static修饰

4.1 静态代码块

如果想要为静态变量初始化,可以直接在静态变量的声明后面直接赋值,也可以使用静态代码块。

4.1.1 语法格式

在代码块的前面加static,就是静态代码块。

1
2
3
4
5
【修饰符】 class{
static{
静态代码块
}
}

4.1.2 静态代码块的特点

  1. 可以有输出语句。

    1. 可以对类的属性和类的声明进行初始化操作。

    2. 不可以对非静态的属性初始化。[即:不可以调用非静态的属性和方法]

    3. 若有多个静态的代码块,那么按照从上到下的顺序依次执行。

    4. 执行顺序: 静态代码块 > 非静态代码块。

    5. 静态代码块随着类的加载而加载,且只执行一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Chinese {

private static String country; //私有静态属性country
private String name; //私有非静态属性name

//非静态代码块
{
System.out.println("非静态代码块,country = " + country);
}
//静态代码块
static {
country = "中国";
System.out.println("静态代码块");
}
//构造器
public Chinese(String name) {
this.name = name;
}
}

public class TestStaticBlock {
public static void main(String[] args) {
//创建TestStaticBlock类然后创建出country=null 执行静态代码块 country=中国 输出静态代码块
Chinese c1 = new Chinese("张三"); //创建对象 执行非静态代码块,country =中国
Chinese c2 = new Chinese("李四"); //创建对象 执行非静态代码块,country =中国
}
}
最终输出:
静态代码块
非静态代码块,country = 中国
非静态代码块,country = 中国

4.2 非静态代码块

4.2.1 语法格式

1
2
3
4
5
6
7
8
9
10
11
【修饰符】 class{
{
非静态代码块
}
【修饰符】 构造器名(){
// 实例初始化代码
}
【修饰符】 构造器名(参数列表){
// 实例初始化代码
}
}

4.2.2 非静态代码块的作用

和构造器一样,也是用于实例变量的初始化等操作。

4.2.3 非静态代码块的意义

如果多个重载的构造器公共代码[先于构造器其他代码执行的],那么可以将这部分代码 —-> 非静态代码块,减少冗余代码。

4.2.4 非静态代码块的执行特点

  1. 可以有输出语句。
    1. 可以对类的属性、类的声明进行初始化操作。
    2. 可以调用 非静态的结构 + 静态的变量或方法。
    3. 若有多个非静态的代码块,那么按照从上到下的顺序依次执行。
    4. 每次创建对象的时候,都会执行一次。
    5. 执行顺序:静态代码块 > 非静态代码块 > 构造器

4.3 举例

举例1:

(1)声明User类,

  • 包含属性:username(String类型),password(String类型),registrationTime(long类型),私有化

  • 包含get/set方法,其中registrationTime没有set方法

  • 包含无参构造,

    • 输出“新用户注册”,
    • registrationTime赋值为当前系统时间,
    • username就默认为当前系统时间值,
    • password默认为“123456”
  • 包含有参构造(String username, String password),

    • 输出“新用户注册”,
    • registrationTime赋值为当前系统时间,
    • username和password由参数赋值
  • 包含public String getInfo()方法,返回:“用户名:xx,密码:xx,注册时间:xx”

(2)编写测试类,测试类main方法的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class UserTest {
public static void main(String[] args) {
User u1 = new User();
System.out.println(u1.getInfo());
User u2 = new User("song","8888");
System.out.println(u2.getInfo());
}
}
最终输出:
新用户注册
用户名:larkkkkkkk,密码:123456,注册时间:1696858612390
新用户注册
用户名:song,密码:8888,注册时间:1696858612400

如果不用非静态代码块,User类是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class User {
private String username;
private String password;
private long registrationTime;
//属性的set和get方法
public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

//没有set方法
public long getRegistrationTime() {
return registrationTime;
}

//使用非静态代码块
{
System.out.println("新用户注册");
this.registrationTime = System.currentTimeMillis();
}

//无参构造器
public User(){
//System.out.println("新用户注册");
//this.registrationTime=System.currentTimeMillis();
username="larkkkkkkk";
password="123456";
}

//有参构造器
public User(String username,String password){
//System.out.println("新用户注册");
//this.registrationTime=System.currentTimeMillis();
this.username=username;
this.password=password;
}

public String getInfo(){
return "用户名:"+getUsername()+",密码:"+getPassword()+",注册时间:"+getRegistrationTime();
}

}

如果提取构造器公共代码到非静态代码块,User类是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//非静态代码块
{
System.out.println("新用户注册");
registrationTime = System.currentTimeMillis();
}

public User() {
username = registrationTime+"";
password = "123456";
}

public User(String username, String password) {
this.username = username;
this.password = password;
}

举例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private static DataSource dataSource = null;

static{
InputStream is = null;
try {
is = DBCPTest.class.getClassLoader().getResourceAsStream("dbcp.properties");
Properties pros = new Properties();
pros.load(is);
//调用BasicDataSourceFactory的静态方法,获取数据源。
dataSource = BasicDataSourceFactory.createDataSource(pros);
} catch (Exception e) {
e.printStackTrace();
}finally{
if(is != null){
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

4.4 小结:实例变量赋值顺序

image-20220325230208941

4.5 练习

练习1:分析加载顺序 — 由父后子,静态先行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
class Root{
//静态代码块
static{
System.out.println("Root的静态初始化块");
}
//非静态代码块
{
System.out.println("Root的普通初始化块");
}
//无参构造器
public Root(){
System.out.println("Root的无参数的构造器");
}
}
class Mid extends Root{
//静态代码块
static{
System.out.println("Mid的静态初始化块");
}
//非静态代码块
{
System.out.println("Mid的普通初始化块");
}
//无参构造器
public Mid(){
//没写this(参数列表)和super(参数列表) --默认读取父类的无参构造器
System.out.println("Mid的无参数的构造器");
}
//有参构造器
public Mid(String msg){
this();//调用无参构造器
System.out.println("Mid的带参数构造器,其参数值:"+ msg);
}
}
class Leaf extends Mid{
//静态代码块
static{
System.out.println("Leaf的静态初始化块");
}
//非静态代码块
{
System.out.println("Leaf的普通初始化块");
}
//无参构造器
public Leaf(){
super("尚硅谷"); //通过super调用父类中有一个字符串参数的构造器
System.out.println("Leaf的构造器");
}
}

public class LeafTest{
public static void main(String[] args){
//创建一个子类对象
new Leaf();
}
}

最终输出:
//顺序: 静态代码块 > 非静态代码块 > 构造器
//先加载父类
Root的静态初始化块
Mid的静态初始化块
Leaf的静态初始化块
//后面的就根据类的调用顺序执行
//加载root类
Root的普通初始化块
Root的无参数的构造器
//加载Mid类
Mid的普通初始化块
Mid的无参数的构造器
Mid的带参数构造器,其参数值:尚硅谷
//最终加载最小子类Leaf
Leaf的普通初始化块
Leaf的构造器

练习2:分析加载顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
class Father {
//静态代码块
static {
System.out.println("11111111111");
}
//非静态代码块
{
System.out.println("22222222222");
}
//无参构造器
public Father() {
System.out.println("33333333333");
}

}

public class Son extends Father {
//静态代码块
static {
System.out.println("44444444444");
}
//非静态代码块
{
System.out.println("55555555555");
}
//无参构造器
public Son() {
System.out.println("66666666666");
}

//执行顺序: 静态代码块 > 非静态代码块 > 构造器
public static void main(String[] args) {
System.out.println("77777777777");
System.out.println("************************");

//创建子类的匿名对象[只创建一次]
new Son();
System.out.println("************************");

//创建子类的匿名对象[只创建一次]
new Son();
System.out.println("************************");

//创建父类的匿名对象[只创建一次]
new Father();
}

}

最终输出:
//先初始化父类和子类的静态代码块(只执行一次)
11111111111
44444444444
77777777777
************************
//先执行父类的非静态代码块和构造器
22222222222
33333333333
//后执行子类的非静态代码块和构造器
55555555555
66666666666
************************
//先执行父类的非静态代码块和构造器
22222222222
33333333333
//后执行子类的非静态代码块和构造器
55555555555
66666666666
************************
22222222222
33333333333

练习3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
class Fu{
//私有静态属性
private static int i = getNum("(1)i");
//私有非静态属性
private int j = getNum("(2)j");
//静态代码块
static{
print("(3)父类静态代码块");
}
//非静态代码块
{
print("(4)父类非静态代码块,又称为构造代码块");
}
//父类无参构造器
Fu(){
print("(5)父类构造器");
}
//print方法
public static void print(String str){
System.out.println(str + "->" + i);
}
//获得数字
public static int getNum(String str){
print(str);
return ++i;
}
}

class Zi extends Fu{
//私有静态属性
private static int k = getNum("(6)k");
//私有非静态属性
private int h = getNum("(7)h");
//静态代码块
static{
print("(8)子类静态代码块");
}
//非静态代码块
{
print("(9)子类非静态代码块,又称为构造代码块");
}
//子类无参构造器
Zi(){
//默认调用父类无参构造器 super()
print("(10)子类构造器");
}
//print方法
public static void print(String str){
System.out.println(str + "->" + k);
}
//获得数字
public static int getNum(String str){
print(str);
return ++k;
}
}

public class Test04 {
public static void main(String[] args) {
//创建子类对象
Zi zi = new Zi();
}
}

最终输出:
//先执行父类和子类的创建 静态的东西
1)i->0
3)父类静态代码块->1
6)k->0
8)子类静态代码块->1
//再执行创建对象时从父类->子类实例化
//父类的实例化属性
//父类的非静态代码块
//父类的构造器
2)j->1
4)父类非静态代码块,又称为构造代码块->2
5)父类构造器->2
7)h->1
9)子类非静态代码块,又称为构造代码块->2
10)子类构造器->2

5. final关键字

5.1 final的意义

final:最终的,不可更改的

5.2 final的使用

5.2.1 final修饰类

表示这个类不能被继承,没有子类。提高安全性,提高程序的可读性。

例如:String类、System类、StringBuffer类

1
2
3
4
5
6
final class Eunuch{//太监类

}
class Son extends Eunuch{//错误

}

5.2.2 final修饰方法

表示这个方法不能被子类重写。

例如:Object类中的getClass()

1
2
3
4
5
6
7
8
9
10
class Father{
public final void method(){
System.out.println("father");
}
}
class Son extends Father{
public void method(){//错误
System.out.println("son");
}
}

5.2.3 final修饰变量

final修饰某个变量(成员变量或局部变量),一旦赋值,它的值就不能被修改,即常量,常量名建议使用大写字母。

例如:final double MY_PI = 3.14;

如果某个成员变量用final修饰后,没有set方法 + 必须初始化(可以显式赋值、或在初始化块赋值、实例变量还可以在构造器中赋值)

  • 修饰成员变量
1
2
3
4
5
6
7
8
9
10
11
12
public final class Test {
public static int totalNumber = 5;
public final int ID;

public Test() {
ID = ++totalNumber; // 可在构造器中给final修饰的“变量”赋值
}
public static void main(String[] args) {
Test t = new Test();
System.out.println(t.ID);
}
}
  • 修饰局部变量:
1
2
3
4
5
6
7
8
public class TestFinal {
public static void main(String[] args){
final int MIN_SCORE ;
MIN_SCORE = 0;
final int MAX_SCORE = 100;
MAX_SCORE = 200; //非法
}
}
  • 错误演示:
1
2
3
4
5
6
7
8
class A {
private final String INFO = "atguigu"; //声明常量

public void print() {
//The final field A.INFO cannot be assigned
//INFO = "尚硅谷"; //不能被赋值 修改
}
}

5.3 笔试题

题1:排错

1
2
3
4
5
6
public class Something {
public int addOne(final int x) {
return ++x; //不能修改
// return x + 1; //可以 因为x没被修改
}
}

题2:排错

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Something {
public static void main(String[] args) {
Other o = new Other();
new Something().addOne(o);
}
public void addOne(final Other o) {
// o = new Other(); //不能重新new一个 只能用o调用方法/调用属性
o.i++;
}
}
class Other {
public int i;
}

6. 抽象类与抽象方法(或abstract关键字)

6.1 由来

举例1:

随着继承层次中一个个新子类的定义,类变得越来越具体,而父类则更一般,更通用。类的设计应该保证父类和子类能够共享特征。有时将一个父类设计得非常抽象,以至于它没有具体的实例,这样的类叫做抽象类。

image-20220325231608838

举例2:

我们声明一些几何图形类:圆、矩形、三角形类等,发现这些类都有共同特征:求面积、求周长。那么这些共同特征应该抽取到一个共同父类:几何图形类中。但是这些方法在父类中又无法给出具体的实现,而是应该交给子类各自具体实现。那么父类在声明这些方法时,就只有方法签名,没有方法体,我们把没有方法体的方法称为抽象方法。Java语法规定,包含抽象方法的类必须是抽象类

6.2 语法格式

  • 抽象类:被abstract修饰的类。
  • 抽象方法:被abstract修饰没有方法体的方法。

抽象类的语法格式

1
2
3
4
5
6
[权限修饰符] abstract class 类名{

}
[权限修饰符] abstract class 类名 extends 父类{

}

抽象方法的语法格式

1
[其他修饰符] abstract 返回值类型 方法名([形参列表]);

注意:抽象方法没有方法体

image-20220517204707255

代码举例:

1
2
3
public abstract class Animal {
public abstract void eat();
}
1
2
3
4
5
public class Cat extends Animal {
public void eat (){
System.out.println("小猫吃鱼和猫粮");
}
}
1
2
3
4
5
6
7
8
9
public class CatTest {
public static void main(String[] args) {
// 创建子类对象
Cat c = new Cat();

// 调用eat方法
c.eat();
}
}

此时的方法重写,是子类对父类抽象方法的完成实现,我们将这种方法重写的操作,也叫做实现方法

6.3 使用说明

  1. 抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。

    理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。

    抽象类是用来被继承的,抽象类的子类必须重写父类的抽象方法,并提供方法体。若没有重写全部的抽象方法,仍为抽象类。

  2. 抽象类中,也有构造方法,是供子类创建对象时,初始化父类成员变量使用的。

    理解:子类的构造方法中,有默认的super()或手动的super(实参列表),需要访问父类构造方法。

  3. 抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。

    理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类结构设计。

  4. 抽象类的子类,必须重写抽象父类中所有的抽象方法,否则,编译无法通过而报错。除非该子类也是抽象类。

    理解:假设不重写所有抽象方法,则类中可能包含抽象方法。那么创建对象后,调用抽象的方法,没有意义。

6.4 注意事项

  • 不能用abstract修饰变量、代码块、构造器;

  • 不能用abstract修饰私有方法、静态方法、final的方法、final的类。

  • 私有方法不能重写 –私有方法不能被重写但是abstract方法必须让子类重写
    避免静态方法使用类进行调用 –方法只能被①类[一定是static方法]和②对象调用[抽象类不能实例化对象] 我现在abstract不能调用类 所以方法也不能是static方法
    final的方法不能被重写 —-抽象类必须有子类重写所有抽象方法
    final修饰的类不能有子类 –抽象类必须有子类重写所有抽象方法

6.5 应用举例1

image-20220325232847872

在航运公司系统中,Vehicle类需要定义两个方法分别计算运输工具的燃料效率行驶距离

问题:卡车(Truck)和驳船(RiverBarge)的燃料效率和行驶距离的计算方法完全不同。Vehicle类不能提供计算方法,但子类可以。

解决方案:Java允许类设计者指定:超类声明一个方法但不提供实现,该方法的实现由子类提供。这样的方法称为抽象方法。有一个或更多抽象方法的类称为抽象类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//Vehicle是一个抽象类,有两个抽象方法。
public abstract class Vehicle{
public abstract double calcFuelEfficiency(); //计算燃料效率的抽象方法
public abstract double calcTripDistance(); //计算行驶距离的抽象方法
}
//卡车
public class Truck extends Vehicle{
@Override
public double calcFuelEfficiency( ){
//写出计算卡车的燃料效率的具体方法
}
@Override
public double calcTripDistance( ){
//写出计算卡车行驶距离的具体方法
}
}
//驳船
public class RiverBarge extends Vehicle{
@Override
public double calcFuelEfficiency( ){
//写出计算驳船的燃料效率的具体方法
}
@Override
public double calcTripDistance( ){
//写出计算驳船行驶距离的具体方法
}
}

6.6 应用举例2:模板方法设计模式(TemplateMethod)

抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造,但子类总体上会保留抽象类的行为方式。

解决的问题

  • 当功能内部一部分实现是确定的,另一部分实现是不确定的【这部分暴露出去给子类实现】。

  • 换句话说,在软件开发中实现一个算法时,整体步骤很固定、通用,这些步骤已经在父类中写好了。但是某些部分易变,易变部分可以抽象出来,供不同子类实现。这就是一种模板模式。

类比举例:英语六级模板

image-20220503145003315

制作月饼的模板:

image-20220517205013997

举例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//父类要是一个抽象类
abstract class Template {
public final void getTime() {
long start = System.currentTimeMillis();
code();
long end = System.currentTimeMillis();
System.out.println("执行时间是:" + (end - start));
}
//抽象方法
public abstract void code();
}
//子类
class SubTemplate extends Template {
//子类必须重写父类所有抽象方法
@Override
public void code() {
for (int i = 0; i < 10000; i++) {
System.out.println(i);
}
}
}

举例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
//抽象类的应用:模板方法的设计模式
public class TemplateMethodTest {
public static void main(String[] args) {
BankTemplateMethod btm = new DrawMoney(); //子类-取款类
btm.process();

BankTemplateMethod btm2 = new ManageMoney(); //子类-理财类
btm2.process();
}
}
abstract class BankTemplateMethod {
// 具体方法
public void takeNumber() {
System.out.println("取号排队");
}

public void evaluate() {
System.out.println("反馈评分");
}

// 抽象方法
public abstract void transact(); // 办理具体的业务 //钩子方法

// 模板方法,把基本操作组合到一起,子类一般不能重写
public final void process() {
//取号
this.takeNumber();
//办理业务
this.transact();// 具体执行时,挂哪个子类,就执行哪个子类的实现代码
//评分
this.evaluate();
}

}

class DrawMoney extends BankTemplateMethod {
@Override
public void transact() {
System.out.println("我要取款!!!");
}
}

class ManageMoney extends BankTemplateMethod {
@Override
public void transact() {
System.out.println("我要理财!我这里有2000万美元!!");
}
}

最终输出:
取号排队
我要取款!!!
反馈评分
取号排队
我要理财!我这里有2000万美元!!
反馈评分

模板方法设计模式是编程中经常用得到的模式。各个框架、类库中都有他的影子,比如常见的有:

  • 数据库访问的封装

  • Junit单元测试

  • JavaWeb的Servlet中关于doGet/doPost方法调用

  • Hibernate中模板程序

  • Spring中JDBCTemlate、HibernateTemplate等

6.7 思考与练习

思考:

问题1:为什么抽象类不可以使用final关键字声明? — 因为final类是不可以被继承的,abstract类必须让子类重写抽象方法

问题2:一个抽象类中可以定义构造器吗? — 可以,要留给子类到时候继承父类成员

问题3:抽象类就是比普通类多定义了抽象方法,除了不能直接进行类的实例化操作之外,并没有任何的不同? — 确实

练习1:

编写一个Employee类,声明为抽象类,包含如下三个属性:name,id,salary。提供必要的构造器和抽象方法:work()。

对于Manager类来说,他既是员工,还具有奖金(bonus)的属性。

请使用继承的思想,设计CommonEmployee类和Manager类,要求类中提供必要的方法进行属性访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//抽象类
public abstract class Employee {
private String name;
private int id;
private double salary;

public Employee(){

}

public Employee(String name, int id, double salary) {
this.name = name;
this.id = id;
this.salary = salary;
}
//抽象方法
public abstract void work();

}

//子类
public class Manager extends Employee{ //继承Employee类
private double bonus;
//必须重写
@Override
public void work() {
System.out.println("我是管理人员,我需要努力工作");
}
}

练习2:软件外包公司外派管理

有一家软件外包公司,可以外派开发人员,该公司有两个角色:普通开发人员Developer和项目经理Manager。他们的关系如下图:

image-20220504164925878

普通开发人员的工作内容是“开发项目”,项目经理的工作内容是“项目管理”。对外的报价是普通开发人员每天500,元,超过60天每天400元。项目经理每天800元,超过60天每天700元。

有一家银行需要1名项目经理、2名开发人员,现场开发90天,计算银行需要付给软件公司的总金额。

提示:创建数组 Employee[] emps = new Employee[3]。其中存储驻场的3名员工。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//抽象类
public abstract class Employee {
private String name;
private int age;

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

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 abstract void work();
public abstract double calMoney(int days);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Developer extends Employee{
private int workExperiences=500;

public Developer(String name,int age){
super(name,age); //调用父类的有参构造器
}

@Override
public void work() {
System.out.println("开发项目");
}

@Override
public double calMoney(int days) {
int sum=0;
//看看超没超过60天
int quyu=days%60;
if(quyu>0){ //如果超过60天
sum+=(this.workExperiences*60+400*(quyu));
return sum;
}
return this.workExperiences*days; //默认是没超过60天
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Manager extends Employee{
private int manageExperiences=800;

public Manager(String name,int age) {
super(name, age); //调用父类的有参构造器
}

@Override
public void work() {
System.out.println("项目管理");
}

@Override
public double calMoney(int days) {
int sum=0;
//看看超没超过60天
int quyu=days%60;
if(quyu>0){
sum+=(this.manageExperiences*60+700*(quyu));
return sum;
}
return this.manageExperiences*days;
}
}
1
2
3
4
5
6
7
8
9
10
11
public class Test{
public static void main(String[] args) {
Employee[] emps=new Employee[3];
//一名项目经理
emps[0]=new Manager("项目经理",26);
//两名开发人员
emps[1]=new Developer("开发人员1",30);
emps[2]=new Developer("开发人员2",30);
System.out.println(emps[0].calMoney(90)+emps[1].calMoney(90)+emps[2].calMoney(90));
}
}

练习3:

创建父类Shape,包含绘制形状的抽象方法draw()。

创建Shape的子类Circle和Rectangle,重写draw()方法,绘制圆形和矩形。

绘制多个圆形和矩形。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//抽象类
public abstract class Shape {
//抽象方法
public abstract void draw();
}
//子类
public class Circle extends Shape{
@Override
public void draw() {
System.out.println("绘制圆形");
}
}
//子类
public class Rectangle extends Shape{
@Override
public void draw() {
System.out.println("绘制矩形");
}
}

练习4:

1、声明抽象父类Person,包含抽象方法public abstract void eat();

1
2
3
4
public abstract class Person{
//抽象方法
public abstract void eat();
}

2、声明子类中国人Chinese,重写抽象方法,打印用筷子吃饭

1
2
3
4
5
6
public class Chinese extends Person{
@Override
public void eat() {
System.out.println("用筷子吃饭");
}
}

3、声明子类美国人American,重写抽象方法,打印用刀叉吃饭

1
2
3
4
5
6
public class American extends Person{
@Override
public void eat() {
System.out.println("用刀叉吃饭");
}
}

4、声明子类印度人Indian,重写抽象方法,打印用手抓饭

1
2
3
4
5
6
public class Indian extends Person{
@Override
public void eat() {
System.out.println("用手抓饭");
}
}

5、声明测试类PersonTest,创建Person数组,存储各国人对象,并遍历数组,调用eat()方法

1
2
3
4
5
6
7
8
9
10
11
public class PersonTest {
public static void main(String[] args) {
Person[] p1=new Person[3];
p1[0]=new Indian();
p1[0].eat();
p1[1]=new Chinese();
p1[1].eat();
p1[2]=new American();
p1[2].eat();
}
}

练习5:工资系统设计

编写工资系统,实现不同类型员工(多态)的按月发放工资。如果当月出现某个Employee对象的生日,则将该雇员的工资增加100元。

实验说明:

(1)定义一个Employee类,该类包含:

private成员变量name,number,birthday,其中birthday 为MyDate类的对象;

abstract方法earnings();

toString()方法输出对象的name,number和birthday。

(2)MyDate类包含:

private成员变量year,month,day ;

toDateString()方法返回日期对应的字符串:xxxx年xx月xx日

(3)定义SalariedEmployee类继承Employee类,实现按月计算工资的员工处理。该类包括:private成员变量monthlySalary;

实现父类的抽象方法earnings(),该方法返回monthlySalary值;toString()方法输出员工类型信息及员工的name,number,birthday。

(4)参照SalariedEmployee类定义HourlyEmployee类,实现按小时计算工资的员工处理。该类包括:

private成员变量wage和hour;

实现父类的抽象方法earnings(),该方法返回wage*hour值;

toString()方法输出员工类型信息及员工的name,number,birthday。

(5)定义PayrollSystem类,创建Employee变量数组并初始化,该数组存放各类雇员对象的引用。利用循环结构遍历数组元素,输出各个对象的类型,name,number,birthday,以及该对象生日。当键盘输入本月月份值时,如果本月是某个Employee对象的生日,还要输出增加工资信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//提示:
//定义People类型的数组People c1[]=new People[10];
//数组元素赋值
c1[0]=new People("John","0001",20);
c1[1]=new People("Bob","0002",19);
//若People有两个子类Student和Officer,则数组元素赋值时,可以使父类类型的数组元素指向子类。
c1[0]=new Student("John","0001",20,85.0);
c1[1]=new Officer("Bob","0002",19,90.5);

public abstract class Employee {
private String name;
private int number;
private MyDate birthday;

public Employee() {
}

public Employee(String name, int number, MyDate birthday) {
this.name = name;
this.number = number;
this.birthday = birthday;
}

public String getName() {
return name;
}

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

public int getNumber() {
return number;
}

public void setNumber(int number) {
this.number = number;
}

public MyDate getBirthday() {
return birthday;
}

public void setBirthday(MyDate birthday) {
this.birthday = birthday;
}

public abstract double earnings();

public String toString(){
return "name = " + name + ",number = " + number +", birthday = " + birthday.toDateString();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SalariedEmployee extends Employee{
private double monthlySalary;//月工资

public SalariedEmployee() {
}

@Override
public double earnings() {
return monthlySalary;
}

public SalariedEmployee(String name, int number, MyDate birthday, double monthlySalary) {
super(name, number, birthday);
this.monthlySalary = monthlySalary;
}

public void setMonthlySalary(double monthlySalary) {
this.monthlySalary = monthlySalary;
}

public String toString(){
return "SalariedEmployee[" + super.toString() + "]";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class HourlyEmployee extends Employee{
private double wage;//单位小时的工资
private int hour;//月工作的小时数

public HourlyEmployee() {
}

public HourlyEmployee(String name, int number, MyDate birthday, double wage, int hour) {
super(name, number, birthday);
this.wage = wage;
this.hour = hour;
}

public double getWage() {
return wage;
}

public void setWage(double wage) {
this.wage = wage;
}

public int getHour() {
return hour;
}

public void setHour(int hour) {
this.hour = hour;
}

@Override
public double earnings() {
return wage * hour;
}

public String toString(){
return "HourlyEmployee[" + super.toString() + "]";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class MyDate {
private int year;
private int month;
private int day;
public MyDate() {
}

public MyDate(int year, int month, int day) {
this.year = year;
this.month = month;
this.day = day;
}

public int getYear() {
return year;
}

public void setYear(int year) {
this.year = year;
}

public int getMonth() {
return month;
}

public void setMonth(int month) {
this.month = month;
}

public int getDay() {
return day;
}

public void setDay(int day) {
this.day = day;
}

public String toDateString(){
return year + "年" + month + "月" + day + "日";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class PayrollSystem {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
Employee[] emps = new Employee[2];

emps[0] = new SalariedEmployee("张小亮",1001,new MyDate(1992,12,21),18000);

emps[1] = new HourlyEmployee("侯少鹏",1002,new MyDate(1997,11,12),240,100);

System.out.println("请输入当前的月份:");
int month = scan.nextInt();

for (int i = 0; i < emps.length; i++) {
System.out.println(emps[i].toString());
System.out.println("工资为:" + emps[i].earnings());
if(month == emps[i].getBirthday().getMonth()){
System.out.println("生日快乐!加薪100");
}
}
scan.close();
}
}

7. 接口(interface)

7.1 类比

生活中大家每天都在用USB接口,那么USB接口与我们今天要学习的接口有什么相同点呢?

USB,(Universal Serial Bus,通用串行总线)是Intel公司开发的总线架构,使得在计算机上添加串行设备(鼠标、键盘、打印机、扫描仪、摄像头、充电器、MP3机、手机、数码相机、移动硬盘等)非常容易。

其实,不管是电脑上的USB插口,还是其他设备上的USB插口都只是遵循了USB规范的一种具体设备而已。

bbcc80f541000c71b81650cfaa770c86

只要设备遵循USB规范的,那么就可以与电脑互联,并正常通信。至于这个设备、电脑是哪个厂家制造的,内部是如何实现的,我们都无需关心。

Java的软件系统会有很多模块组成,那么各个模块之间也应该采用这种面向接口低耦合,为系统提供更好的可扩展性和可维护性。

7.2 概述

接口就是规范,定义的是一组规则,体现了现实世界中“如果你是/要…则必须能…”的思想。继承是一个”是不是”的is-a关系,而接口实现则是 “能不能”的has-a关系。

  • 例如:电脑都预留了可以插入USB设备的USB接口,USB接口具备基本的数据传输的开启功能和关闭功能。你能不能用USB进行连接,或是否具备USB通信功能,就看你能否遵循USB接口规范
image-20220517211517846
  • 例如:Java程序是否能够连接使用某种数据库产品,那么要看该数据库产品能否实现Java设计的JDBC规范
image-20220325235434103

接口的本质是契约、标准、规范,就像我们的法律一样。制定好后大家都要遵守。

7.3 定义格式

接口的定义,它与定义类方式相似,但是使用 interface 关键字。它也会被编译成.class文件,但一定要明确它并不是类,而是另外一种引用数据类型。

引用数据类型:数组,类,枚举,接口,注解。

7.3.1 接口的声明格式

1
2
3
4
5
6
7
8
9
[修饰符] interface 接口名{
//接口的成员列表:
// 公共的静态常量
// 公共的抽象方法

// 公共的默认方法(JDK1.8以上)
// 公共的静态方法(JDK1.8以上)
// 私有方法(JDK1.9以上)
}

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.atguigu.interfacetype;

public interface USB3{
//静态常量
long MAX_SPEED = 500*1024*1024;//500MB/s

//抽象方法
void in();
void out();

//默认方法
default void start(){
System.out.println("开始");
}
default void stop(){
System.out.println("结束");
}

//静态方法
static void show(){
System.out.println("USB 3.0可以同步全速地进行读写操作");
}
}

7.3.2 接口的成员说明

在JDK8.0 之前,接口中只允许出现:

(1)公共的静态的常量:其中public static final可以省略

(2)公共的抽象的方法:其中public abstract可以省略

理解:接口是从多个相似类中抽象出来的规范,不需要提供具体实现

在JDK8.0 时,接口中允许声明默认方法静态方法

(3)公共的默认的方法:其中public 可以省略,建议保留,但是default不能省略

(4)公共的静态的方法:其中public 可以省略,建议保留,但是static不能省略

在JDK9.0 时,接口又增加了:

(5)私有方法

除此之外,接口中没有构造器,没有初始化块,因为接口中没有成员变量需要动态初始化。

7.4 接口的使用规则

1、类实现接口(implements)

接口不能创建对象,但是可以被类实现(implements ,类似于被继承)。

类与接口的关系为实现关系,即类实现接口,该类可以称为接口的实现类。实现的动作类似继承,格式相仿,只是关键字不同,实现使用 implements关键字。

1
2
3
4
5
6
7
8
9
【修饰符】 class 实现类  implements 接口{
// 重写接口中抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
// 重写接口中默认方法【可选】
}

【修饰符】 class 实现类 extends 父类 implements 接口{
// 重写接口中抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
// 重写接口中默认方法【可选】
}
image-20220514163212312

注意:

  1. 如果接口的实现类是非抽象类,那么必须重写接口中所有抽象方法

  2. 默认方法可以选择保留,也可以重写。

    重写时,default单词就不要再写了,它只用于在接口中表示默认方法,到类中就没有默认方法的概念了

  3. 接口中的静态方法不能被继承也不能被重写

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
interface USB{		// 
public void start() ;
public void stop() ;
}
class Computer{
public static void show(USB usb){
usb.start() ;
System.out.println("=========== USB 设备工作 ========") ;
usb.stop() ;
}
};
class Flash implements USB{
public void start(){ // 重写方法
System.out.println("U盘开始工作。") ;
}
public void stop(){ // 重写方法
System.out.println("U盘停止工作。") ;
}
};
class Print implements USB{
public void start(){ // 重写方法
System.out.println("打印机开始工作。") ;
}
public void stop(){ // 重写方法
System.out.println("打印机停止工作。") ;
}
};
public class InterfaceDemo{
public static void main(String args[]){
Computer.show(new Flash()) ;
Computer.show(new Print()) ;

c.show(new USB(){
public void start(){
System.out.println("移动硬盘开始运行");
}
public void stop(){
System.out.println("移动硬盘停止运行");
}
});
}
};

2、接口的多实现(implements)

之前学过,在继承体系中,一个类只能继承一个父类。而对于接口而言,一个类是可以实现多个接口的,这叫做接口的多实现。并且,一个类能继承一个父类,同时实现多个接口。

实现格式:

1
2
3
4
5
6
7
8
9
【修饰符】 class 实现类  implements 接口1,接口2,接口3。。。{
// 重写接口中所有抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
// 重写接口中默认方法【可选】
}

【修饰符】 class 实现类 extends 父类 implements 接口1,接口2,接口3。。。{
// 重写接口中所有抽象方法【必须】,当然如果实现类是抽象类,那么可以不重写
// 重写接口中默认方法【可选】
}

接口中,有多个抽象方法时,实现类必须重写所有抽象方法。如果抽象方法有重名的,只需要重写一次

举例:

image-20220514163311418 image-20220325235321778

1562216188519

定义多个接口:

1
2
3
public interface A {
void showA();
}
1
2
3
public interface B {
void showB();
}

定义实现类:

1
2
3
4
5
6
7
8
9
10
11
public class C implements A,B {
@Override
public void showA() {
System.out.println("showA");
}

@Override
public void showB() {
System.out.println("showB");
}
}

测试类

1
2
3
4
5
6
7
public class TestC {
public static void main(String[] args) {
C c = new C();
c.showA();
c.showB();
}
}

3、接口的多继承(extends)

一个接口能继承另一个或者多个接口,接口的继承也使用 extends 关键字,子接口继承父接口的方法。

定义父接口:

1
2
3
4
5
public interface Chargeable {
void charge();
void in();
void out();
}

定义子接口:

1
2
3
public interface UsbC extends Chargeable,USB3 {
void reverse();
}

定义子接口的实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TypeCConverter implements UsbC {
@Override
public void reverse() {
System.out.println("正反面都支持");
}

@Override
public void charge() {
System.out.println("可充电");
}

@Override
public void in() {
System.out.println("接收数据");
}

@Override
public void out() {
System.out.println("输出数据");
}
}

所有父接口的抽象方法都有重写。

方法签名相同的抽象方法只需要实现一次。

4、接口与实现类对象构成多态引用

实现类实现接口,类似于子类继承父类,因此,接口类型的变量与实现类的对象之间,也可以构成多态引用。通过接口类型的变量调用方法,最终执行的是你new的实现类对象实现的方法体。

接口的不同实现类:

1
2
3
4
5
6
7
8
9
10
11
public class Mouse implements USB3 {
@Override
public void out() {
System.out.println("发送脉冲信号");
}

@Override
public void in() {
System.out.println("不接收信号");
}
}
1
2
3
4
5
6
7
8
9
10
11
public class KeyBoard implements USB3{
@Override
public void in() {
System.out.println("不接收信号");
}

@Override
public void out() {
System.out.println("发送按键信号");
}
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.atguigu.interfacetype;

public class TestComputer {
public static void main(String[] args) {
Computer computer = new Computer();
USB3 usb = new Mouse();
computer.setUsb(usb);
usb.start();
usb.out();
usb.in();
usb.stop();
System.out.println("--------------------------");

usb = new KeyBoard();
computer.setUsb(usb);
usb.start();
usb.out();
usb.in();
usb.stop();
System.out.println("--------------------------");

usb = new MobileHDD();
computer.setUsb(usb);
usb.start();
usb.out();
usb.in();
usb.stop();
}
}

5、使用接口的静态成员

接口不能直接创建对象,但是可以通过接口名直接调用接口的静态方法和静态常量。

1
2
3
4
5
6
7
8
public class TestUSB3 {
public static void main(String[] args) {
//通过“接口名.”调用接口的静态方法 (JDK8.0才能开始使用)
USB3.show();
//通过“接口名.”直接使用接口的静态常量
System.out.println(USB3.MAX_SPEED);
}
}

6、使用接口的非静态方法

  • 对于接口的静态方法,直接使用“接口名.”进行调用即可
    • 也只能使用“接口名.”进行调用,不能通过实现类的对象进行调用
  • 对于接口的抽象方法、默认方法,只能通过实现类对象才可以调用
    • 接口不能直接创建对象,只能创建实现类的对象
1
2
3
4
5
6
7
8
9
10
11
public class TestMobileHDD {
public static void main(String[] args) {
//创建实现类对象
MobileHDD b = new MobileHDD();
//通过实现类对象调用重写的抽象方法,以及接口的默认方法,如果实现类重写了就执行重写的默认方法,如果没有重写,就执行接口中的默认方法
b.start();
b.in();
b.stop();
Usb3.show();
}
}

7.5 JDK8中相关冲突问题

7.5.1 默认方法冲突问题

(1)类优先原则

当一个类,既继承一个父类,又实现若干个接口时,父类中的成员方法与接口中的抽象方法重名,子类就近选择执行父类的成员方法。代码如下:

定义接口:

1
2
3
4
5
6
7
package com.atguigu.interfacetype;

public interface Friend {
default void date(){//约会
System.out.println("吃喝玩乐");
}
}

定义父类:

1
2
3
4
5
6
7
package com.atguigu.interfacetype;

public class Father {
public void date(){//约会
System.out.println("爸爸约吃饭");
}
}

定义子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.atguigu.interfacetype;

public class Son extends Father implements Friend {
@Override
public void date() {
//(1)不重写默认保留父类的
//(2)调用父类被重写的
// super.date();
//(3)保留父接口的
// Friend.super.date();
//(4)完全重写
System.out.println("跟康师傅学Java");
}
}

定义测试类:

1
2
3
4
5
6
7
8
package com.atguigu.interfacetype;

public class TestSon {
public static void main(String[] args) {
Son s = new Son();
s.date();
}
}

(2)接口冲突(左右为难)

  • 当一个类同时实现了多个父接口,而多个父接口中包含方法签名相同的默认方法时,怎么办呢?

无论你多难抉择,最终都是要做出选择的。

声明接口:

1
2
3
4
5
6
7
package com.atguigu.interfacetype;

public interface BoyFriend {
default void date(){//约会
System.out.println("神秘约会");
}
}

选择保留其中一个,通过“接口名.super.方法名“的方法选择保留哪个接口的默认方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.atguigu.interfacetype;

public class Girl implements Friend,BoyFriend{

@Override
public void date() {
//(1)保留其中一个父接口的
// Friend.super.date();
// BoyFriend.super.date();
//(2)完全重写
System.out.println("跟康师傅学Java");
}

}

测试类

1
2
3
4
5
6
7
8
package com.atguigu.interfacetype;

public class TestGirl {
public static void main(String[] args) {
Girl girl = new Girl();
girl.date();
}
}
  • 当一个子接口同时继承了多个接口,而多个父接口中包含方法签名相同的默认方法时,怎么办呢?

另一个父接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.atguigu.interfacetype;

public interface USB2 {
//静态常量
long MAX_SPEED = 60*1024*1024;//60MB/s

//抽象方法
void in();
void out();

//默认方法
public default void start(){
System.out.println("开始");
}
public default void stop(){
System.out.println("结束");
}

//静态方法
public static void show(){
System.out.println("USB 2.0可以高速地进行读写操作");
}
}

子接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.atguigu.interfacetype;

public interface USB extends USB2,USB3 {
@Override
default void start() {
System.out.println("Usb.start");
}

@Override
default void stop() {
System.out.println("Usb.stop");
}
}

小贴士:

子接口重写默认方法时,default关键字可以保留。

子类重写默认方法时,default关键字不可以保留。

7.5.2 常量冲突问题

  • 当子类继承父类又实现父接口,而父类中存在与父接口常量同名的成员变量,并且该成员变量名在子类中仍然可见。
  • 当子类同时实现多个接口,而多个接口存在相同同名常量。

此时在子类中想要引用父类或父接口的同名的常量或成员变量时,就会有冲突问题。

父类和父接口:

1
2
3
4
5
package com.atguigu.interfacetype;

public class SuperClass {
int x = 1;
}
1
2
3
4
5
6
package com.atguigu.interfacetype;

public interface SuperInterface {
int x = 2;
int y = 2;
}
1
2
3
4
5
package com.atguigu.interfacetype;

public interface MotherInterface {
int x = 3;
}

子类:

1
2
3
4
5
6
7
8
9
10
11
package com.atguigu.interfacetype;

public class SubClass extends SuperClass implements SuperInterface,MotherInterface {
public void method(){
// System.out.println("x = " + x);//模糊不清
System.out.println("super.x = " + super.x);
System.out.println("SuperInterface.x = " + SuperInterface.x);
System.out.println("MotherInterface.x = " + MotherInterface.x);
System.out.println("y = " + y);//没有重名问题,可以直接访问
}
}

7.6 接口的总结与面试题

  • 接口本身不能创建对象【没有构造器】,只能创建接口的实现类对象,接口类型的变量可以与实现类对象构成多态引用。
  • 声明接口用interface,接口的成员声明有限制:
    • (1)公共的静态常量 public static final可以省略
    • (2)公共的抽象方法 public abstract可以省略
    • (3)公共的默认方法(JDK8.0 及以上)
    • (4)公共的静态方法(JDK8.0 及以上)
    • (5)私有方法(JDK9.0 及以上)
  • 类可以实现(implements)多个接口。如果实现类不是抽象类,就必须实现接口中所有的抽象方法。如果实现类既要继承父类又要实现父接口,那么继承(extends)在前,实现(implements)在后
  • 接口可以继承(extends)多个接口
  • 接口的默认方法可以选择重写/不重写。如果有冲突问题,另行处理。子类重写父接口的默认方法,要去掉default,子接口重写父接口的默认方法,不要去掉default。
  • 接口的静态方法不能被继承,也不能被重写。接口的静态方法只能通过“接口名.静态方法名”进行调用。

面试题

1、为什么接口中只能声明公共的静态的常量?

因为接口是标准规范,那么在规范中需要声明一些底线边界值,当实现者在实现这些规范时,不能去随意修改和触碰这些底线,否则就有“危险”。

例如:USB1.0规范中规定最大传输速率是1.5Mbps,最大输出电流是5V/500mA

​ USB3.0规范中规定最大传输速率是5Gbps(500MB/s),最大输出电流是5V/900mA

例如:尚硅谷学生行为规范中规定学员,早上8:25之前进班,晚上21:30之后离开等等。

2、为什么JDK8.0 之后允许接口定义静态方法和默认方法呢?因为它违反了接口作为一个抽象标准定义的概念。

静态方法:因为之前的标准类库设计中,有很多Collection/Colletions或者Path/Paths这样成对的接口和类,后面的类中都是静态方法,而这些静态方法都是为前面的接口服务的,那么这样设计一对API,不如把静态方法直接定义到接口中使用和维护更方便。

默认方法:(1)我们要在已有的老版接口中提供新方法时,如果添加抽象方法,就会涉及到原来使用这些接口的类就会有问题,那么为了保持与旧版本代码的兼容性,只能允许在接口中定义默认方法实现。比如:Java8中对Collection、List、Comparator等接口提供了丰富的默认方法。(2)当我们接口的某个抽象方法,在很多实现类中的实现代码是一样的,此时将这个抽象方法设计为默认方法更为合适,那么实现类就可以选择重写,也可以选择不重写。

3、为什么JDK1.9要允许接口定义私有方法呢?因为我们说接口是规范,规范是需要公开让大家遵守的。

私有方法:因为有了默认方法和静态方法这样具有具体实现的方法,那么就可能出现多个方法由共同的代码可以抽取,而这些共同的代码抽取出来的方法又只希望在接口内部使用,所以就增加了私有方法。

7.7 接口与抽象类之间的对比

image-20220328002053452

在开发中,常看到一个类要么继承抽象类,要么实现接口。

7.8 练习

笔试题:排错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface A {
int x = 0;
}
class B {
int x = 1;
}
class C extends B implements A {
public void pX() {
System.out.println(x); //没标清楚是谁的x属性
System.out.println(super.x); //输出B的属性
}
public static void main(String[] args) {
new C().pX();
}
}

笔试题:排错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
interface Playable {
void play(); //public abstract可以省略
}

interface Bounceable {
void play(); //public abstract可以省略
}

interface Rollable extends Playable, Bounceable { //子接口继承多个父接口
Ball ball = new Ball("PingPang");
}

class Ball implements Rollable { //子类实现接口
private String name;
//get方法
public String getName() {
return name;
}
//有参构造器
public Ball(String name) {
this.name = name;
}
//重写方法
@Override
public void play() {
ball = new Ball("Football"); //必须Ball ball = new Ball("Football");
System.out.println(ball.getName());
}
}

练习1:

定义一个接口用来实现两个对象的比较。

1
2
3
4
interface CompareObject{
//若返回值是 0 , 代表相等; 若为正数,代表当前对象大;负数代表当前对象小
public int compareTo(Object o);
}

定义一个Circle类,声明redius属性,提供getter和setter方法

定义一个ComparableCircle类,继承Circle类并且实现CompareObject接口。在ComparableCircle类中给出接口中方法compareTo的实现体,用来比较两个圆的半径大小。

定义一个测试类InterfaceTest,创建两个ComparableCircle对象,调用compareTo方法比较两个类的半径大小。

思考:参照上述做法定义矩形类Rectangle和ComparableRectangle类,在ComparableRectangle类中给出compareTo方法的实现,比较两个矩形的面积大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class Circle {
private double radius;
//无参构造器
public Circle() {
}
//有参构造器
public Circle(double radius) {
this.radius = radius;
}
public double getRadius(){
return radius;
}
public void setRadius(double radius){
this.radius=radius;
}

@Override
public String toString() {
return "Circle{" +"radius=" + radius +'}';
}
}

//接口
public interface CompareObject {
//若返回值是0,代表相等
//若为正数,代表当前对象大
//若为负数,代表当前对象小
public int conpareTo(Object o); //抽象方法
}

//继承Circle类 实现CompareObject接口
public class ComparableCircle extends Circle implements CompareObject{ //继承Circle类 实现CompareObject接口
public ComparableCircle() {
}

public ComparableCircle(double radius) {
super(radius);
}

@Override
public int conpareTo(Object o) {
if(this == o){ //如果当前比较的两个对象一致 返回0
return 0;
}
if(o instanceof ComparableCircle){ //o属于圆类
ComparableCircle c = (ComparableCircle)o; //类型强制向下转型
return Double.compare(this.getRadius(),c.getRadius()); //调用Double.compare方法对比
}else{
return 2; //如果输入的类型不匹配,则返回2
}
}

}

//test测试
public class InterfaceTest {
public static void main(String[] args) {
ComparableCircle c1=new ComparableCircle();
ComparableCircle c2=new ComparableCircle();
System.out.println(c1.conpareTo(c2));
}
}

练习2:交通工具案例

阿里的一个工程师,声明的属性和方法如下:

image-20220504172547709

其中,有一个乘坐交通工具的方法takingVehicle(),在此方法中调用交通工具的run()。为了出行方便,他买了一辆捷安特自行车、一辆雅迪电动车和一辆奔驰轿车。这里涉及到的相关类及接口关系如下:

image-20220504172918861

其中,电动车增加动力的方式是充电,轿车增加动力的方式是加油。在具体交通工具的run()中调用其所在类的相关属性信息。

请编写相关代码,并测试。

提示:创建Vehicle[]数组,保存阿里工程师的三辆交通工具,并分别在工程师的takingVehicle()中调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Developer {
private String name;
private int age;
//set和get方法
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 void takingVehicle(Vehicle vehicle){
vehicle.run();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public abstract class Vehicle {  //抽象类
private String brand;
private String color;

public Vehicle(){

}

public Vehicle(String brand,String color){ //子类调用
this.brand=brand;
this.color=color;
}
//set和get方法
public String getBrand() {
return brand;
}

public void setBrand(String brand) {
this.brand = brand;
}

public String getColor() {
return color;
}

public void setColor(String color) {
this.color = color;
}

public abstract void run(); //抽象方法

}
1
2
3
4
public interface IPower {
//抽象方法
public abstract void power(); //电车和骑车的充电功能
}
1
2
3
4
5
6
7
8
9
10
11
public class Bycicle extends Vehicle{
public Bycicle(String brand,String color){
super(brand,color); //调用父类有参构造器
}
//实现抽象类父类的方法
@Override
public void run() {
System.out.println("开的是:"+getBrand()+",颜色是:"+getColor());
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ElectricVehicle extends Vehicle implements IPower{
public ElectricVehicle(String brand,String color){
super(brand,color); //调用父类构造器
}
//实现接口方法
@Override
public void power() {
System.out.println("充电");
}
//实现抽象类父类方法
@Override
public void run() {
System.out.println("开的是:"+super.getBrand()+",颜色是:"+super.getColor());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Car extends Vehicle implements IPower{
private String carNumber;
public Car(String brand,String color,String carNumber){
super(brand,color); //调用父类构造器
this.carNumber=carNumber;
}

public String getCarNumber() {
return carNumber;
}

public void setCarNumber(String carNumber) {
this.carNumber = carNumber;
}

//实现接口方法
@Override
public void power() {
System.out.println("加油");
}
//实现抽象类父类方法
@Override
public void run() {
System.out.println("开的是:"+super.getBrand()+",颜色是:"+super.getColor()+",开的车号码牌是:"+this.getCarNumber());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class DeveloperTest {
public static void main(String[] args) {
Developer test=new Developer();
Vehicle[] vehicles=new Vehicle[3];
//创建三个交通工具
vehicles[0]=new Bycicle("捷安特","黄色");
vehicles[1]=new Car("奥迪","黄色","1231123");
vehicles[2]=new ElectricVehicle("电动车","绿色");
test.takingVehicle(vehicles[0]);
test.takingVehicle(vehicles[1]);
test.takingVehicle(vehicles[2]);
}
}

最终代码结果如下:

image-20231011102622680

8. 内部类(InnerClass)

8.1 概述

8.1.1 什么是内部类

将一个类B定义在另一个类A里面,里面的那个类B就称为内部类(InnerClass),类A则称为外部类(OuterClass)

8.1.2 为什么要声明内部类呢

具体来说,当一个事物A的内部,还有一个部分需要一个完整的结构B进行描述,而这个内部的完整的结构B又只为外部事物A提供服务,不在其他地方单独使用,那么整个内部的完整结构B最好使用内部类。

总的来说,遵循高内聚、低耦合的面向对象开发原则。

8.1.3 内部类的分类

根据内部类声明的位置(如同变量的分类),我们可以分为:

image-20221124223912529

8.2 成员内部类

8.2.1 概述

如果成员内部类中不使用外部类的非静态成员,那么通常将内部类声明为静态内部类,否则声明为非静态内部类。

语法格式:

1
2
3
4
[修饰符] class 外部类{
[其他修饰符] [static] class 内部类{
}
}

成员内部类的使用特征,概括来讲有如下两种角色:

  • 成员内部类作为类的成员
    • 和外部类不同,Inner class还可以声明为private或protected;
    • 可以调用外部类的结构。(注意:在静态内部类中不能使用外部类的非静态成员)
    • Inner class 可以声明为static的,但此时就不能再使用外层类的非static的成员变量;
  • 成员内部类作为
    • 可以在内部定义属性、方法、构造器等结构
    • 可以继承自己的想要继承的父类,实现自己想要实现的父接口们,和外部类的父类和父接口无关
    • 可以声明为abstract类 ,因此可以被其它的内部类继承
    • 可以声明为final的,表示不能被继承
    • 编译以后生成OuterClass$InnerClass.class字节码文件(也适用于局部内部类)

注意点:

  1. 外部类访问成员内部类的成员,需要“内部类.成员”或“内部类对象.成员”的方式

  2. 成员内部类可以直接使用外部类的所有成员,包括私有的数据

  3. 当想要在外部类的静态成员部分使用内部类时,可以考虑内部类声明为静态的

8.2.2 创建成员内部类对象

  • 实例化静态内部类
1
2
外部类名.静态内部类名 变量 = 外部类名.静态内部类名();
变量.非静态方法();
  • 实例化非静态内部类
1
2
3
外部类名 变量1 = new 外部类();
外部类名.非静态内部类名 变量2 = 变量1.new 非静态内部类名();
变量2.非静态方法();

8.2.3 举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
public class TestMemberInnerClass {
public static void main(String[] args) {
//创建静态内部类实例,并调用方法
Outer.StaticInner inner = new Outer.StaticInner();
inner.inFun();
//调用静态内部类静态方法
Outer.StaticInner.inMethod();

System.out.println("*****************************");

//创建非静态内部类实例(方式1),并调用方法
Outer outer = new Outer();
Outer.NoStaticInner inner1 = outer.new NoStaticInner();
inner1.inFun();

//创建非静态内部类实例(方式2)
Outer.NoStaticInner inner2 = outer.getNoStaticInner();
inner1.inFun();
}
}
//外部类
class Outer{
private static String a = "外部类的静态a";
private static String b = "外部类的静态b";
private String c = "外部类对象的非静态c";
private String d = "外部类对象的非静态d";
//内部静态类
static class StaticInner{
private static String a ="静态内部类的静态a";
private String c = "静态内部类对象的非静态c";
public static void inMethod(){
System.out.println("Inner.a = " + a);
System.out.println("Outer.a = " + Outer.a);
System.out.println("b = " + b);
}
public void inFun(){
System.out.println("Inner.inFun");
System.out.println("Outer.a = " + Outer.a);
System.out.println("Inner.a = " + a);
System.out.println("b = " + b);
System.out.println("c = " + c);
// System.out.println("d = " + d);//不能访问外部类的非静态成员
}
}

class NoStaticInner{
private String a = "非静态内部类对象的非静态a";
private String c = "非静态内部类对象的非静态c";

public void inFun(){
System.out.println("NoStaticInner.inFun");
System.out.println("Outer.a = " + Outer.a);
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println("Outer.c = " + Outer.this.c);
System.out.println("c = " + c);
System.out.println("d = " + d);
}
}


public NoStaticInner getNoStaticInner(){
return new NoStaticInner();
}
}
最终输出:
Inner.inFun
Outer.a = 外部类的静态a
Inner.a = 静态内部类的静态a
b = 外部类的静态b
c = 静态内部类对象的非静态c
Inner.a = 静态内部类的静态a
Outer.a = 外部类的静态a
b = 外部类的静态b
*****************************
NoStaticInner.inFun
Outer.a = 外部类的静态a
a = 非静态内部类对象的非静态a
b = 外部类的静态b
Outer.c = 外部类对象的非静态c
c = 非静态内部类对象的非静态c
d = 外部类对象的非静态d
NoStaticInner.inFun
Outer.a = 外部类的静态a
a = 非静态内部类对象的非静态a
b = 外部类的静态b
Outer.c = 外部类对象的非静态c
c = 非静态内部类对象的非静态c
d = 外部类对象的非静态d

8.3 局部内部类

8.3.1 非匿名局部内部类

语法格式:

1
2
3
4
5
6
[修饰符] class 外部类{
[修饰符] 返回值类型 方法名(形参列表){
[final/abstract] class 内部类{
}
}
}
  • 编译后有自己的独立的字节码文件,只不过在内部类名前面冠以外部类名、$符号、编号。
    • 这里有编号是因为同一个外部类中,不同的方法中存在相同名称的局部内部类
  • 和成员内部类不同的是,它前面不能有权限修饰符等
  • 局部内部类如同局部变量一样,有作用域
  • 局部内部类中是否能访问外部类的非静态的成员,取决于所在的方法

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public class TestLocalInner {
public static void main(String[] args) {
Outer.outMethod(); //调用外部类的静态方法
//输出Outer.outMethod
//创建局部内部类的对象
//调用局部内部类的方法 输出Inner.inMehtod
//输出 外部类方法属性 局部变量c
System.out.println("-------------------");

Outer out = new Outer(); //创建外部类对象
out.outTest(); //调用外部类非静态对象
//创建局部内部类的对象
//调用局部内部类的方法 输出Inner.inMethod1
System.out.println("-------------------");

Runner runner = Outer.getRunner(); //创建外部类对象
runner.run(); //调用实现接口的方法
//输出LocalRunner.run

}
}

class Outer{ //外部类

public static void outMethod(){
System.out.println("Outer.outMethod");
final String c = "局部变量c";
//局部内部类
class Inner{ //内部类
public void inMethod(){
System.out.println("Inner.inMethod");
System.out.println(c);
}
}

Inner in = new Inner();
in.inMethod();
}

public void outTest(){
//局部内部类
class Inner{
public void inMethod1(){
System.out.println("Inner.inMethod1");
}
}

Inner in = new Inner();
in.inMethod1();
}

public static Runner getRunner(){
class LocalRunner implements Runner{ //实现接口
@Override
public void run() { //重写run方法
System.out.println("LocalRunner.run");
}
}
return new LocalRunner();
}

}
//接口
interface Runner{
public abstract void run(); //抽象方法
}
最终输出:
Outer.outMethod
Inner.inMethod
局部变量c
-------------------
Inner.inMethod1
-------------------
LocalRunner.run

8.3.2 匿名内部类

因为考虑到这个子类或实现类是一次性的,那么我们“费尽心机”的给它取名字,就显得多余。那么我们完全可以使用匿名内部类的方式来实现,避免给类命名的问题。

1
2
3
new 父类([实参列表]){
重写方法...
}
1
2
3
new 父接口(){
重写方法...
}

举例1:使用匿名内部类的对象直接调用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface A{
public abstract void a(); //抽象方法
}
public class Test{
public static void main(String[] args){
new A(){
@Override
public void a() { //重写接口方法
System.out.println("aaaa");
}
}.a(); //相当于 new 实现类(); 然后调用.a()方法
}
}
最终输出:
aaaa

举例2:通过父类或父接口的变量多态引用匿名内部类的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface A{
void a();
}
public class Test{
public static void main(String[] args){
A obj = new A(){
@Override
public void a() {
System.out.println("aaaa");
}
};
obj.a(); // 然后调用.a()方法
}
}

举例3:匿名内部类的对象作为实参

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface A{
void method();
}
public class Test{
public static void test(A a){ //静态方法
a.method();
}

public static void main(String[] args){
//直接Test.test(A);这样 其中的A属于匿名内部类的对象
test(new A(){
@Override
public void method() {
System.out.println("aaaa");
}
});
}
}

8.4 练习

练习:判断输出结果为何?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//外部类
public class Test { //外部类
//无参构造器
public Test() {
Inner s1 = new Inner(); //创建内部类对象
s1.a = 10;
Inner s2 = new Inner(); //创建内部类对象
s2.a = 20;
//创建内部类对象
Test.Inner s3 = new Test.Inner();
System.out.println(s3.a); //输出s3的a属性
}
//非静态内部类
class Inner {
public int a = 5;
}

//main执行
public static void main(String[] args) {
//创建外部类对象
//直接走无参构造器
//创建内部类对象s1 获得s1.a=5 更改为10
//创建内部类对象s2 获得s2.a=5 更改为20
//创建内部类对象s3 获得s3.a=5 输出5
Test t = new Test();
System.out.println("-------");
//创建内部类对象
//常见内部类对象r 获得r.a=5 输出5
Inner r = t.new Inner();
//输出内部类对象的a属性
//直接输出5
System.out.println(r.a);
}
}
最终输出:
5
---------------------
5

练习2:

编写一个匿名内部类,它继承Object,并在匿名内部类中,声明一个方法public void test()打印尚硅谷。

请编写代码调用这个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
//第一种方式
public class Test02 {
public static void main(String[] args) {
//匿名内部类 直接创建Object的对象
new Object(){
public void test(){
System.out.println("尚硅谷");
}
}.test();

}
}
//二和三方式没办法解决 --因为没被重写

9. 枚举类

9.1 概述

  • 枚举类型本质上也是一种类,只不过是这个类的对象有限的固定几个不让用户随意创建

  • 枚举类的例子:

    • 星期:Monday(星期一)……Sunday(星期天)
    • 性别:Man(男)、Woman(女)
    • 月份:January(1月)……December(12月)
    • 季节:Spring(春节)……Winter(冬天)
    • 三原色:red(红色)、green(绿色)、blue(蓝色)
    • 支付方式:Cash(现金)、WeChatPay(微信)、Alipay(支付宝)、BankCard(银行卡)、CreditCard(信用卡)
    • 就职状态:Busy(忙碌)、Free(空闲)、Vocation(休假)、Dimission(离职)
    • 订单状态:Nonpayment(未付款)、Paid(已付款)、Fulfilled(已配货)、Delivered(已发货)、Checked(已确认收货)、Return(退货)、Exchange(换货)、Cancel(取消)
    • 线程状态:创建、就绪、运行、阻塞、死亡
  • 若枚举只有一个对象, 则作为一种单例模式的实现。

  • 枚举类的实现:

    • 在JDK5.0 之前,需要程序员自定义枚举类型。
    • 在JDK5.0 之后,Java支持enum关键字来快速定义枚举类型。

9.2 定义枚举类(JDK5.0 之前)

在JDK5.0 之前如何声明枚举类呢?

  • 私有化类的构造器,保证不能在类的外部创建其对象
  • 在类的内部创建枚举类的实例。声明为:public static final ,对外暴露这些常量对象
  • 对象如果有实例变量,应该声明为private final(建议,不是必须),并在构造器中初始化

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Season{
private final String SEASONNAME;//季节的名称
private final String SEASONDESC;//季节的描述
private Season(String seasonName,String seasonDesc){
this.SEASONNAME = seasonName;
this.SEASONDESC = seasonDesc;
}
public static final Season SPRING = new Season("春天", "春暖花开");
public static final Season SUMMER = new Season("夏天", "夏日炎炎");
public static final Season AUTUMN = new Season("秋天", "秋高气爽");
public static final Season WINTER = new Season("冬天", "白雪皑皑");

@Override
public String toString() {
return "Season{" +"SEASONNAME='" + SEASONNAME + '\'' +", SEASONDESC='" + SEASONDESC + '\'' +'}';
}
}
class SeasonTest{
public static void main(String[] args) {
System.out.println(Season.AUTUMN);
}
}

9.3 定义枚举类(JDK5.0 之后)

9.3.1 enum关键字声明枚举

1
2
3
4
5
6
7
8
【修饰符】 enum 枚举类名{
常量对象列表
}

【修饰符】 enum 枚举类名{
常量对象列表;
对象的实例变量列表;
}

举例1:

1
2
3
public enum Week {
MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY;
}
1
2
3
4
5
6
public class TestEnum {
public static void main(String[] args) {
Season spring = Season.SPRING;
System.out.println(spring);
}
}

9.3.2 enum方式定义的要求和特点

  • 枚举类的常量对象列表必须在枚举类的首行,因为是常量,所以建议大写。
  • 列出的实例系统会自动添加 public static final 修饰。
  • 如果常量对象列表后面没有其他代码,那么“;”可以省略,否则不可以省略“;”。
  • 编译器给枚举类默认提供的是private的无参构造,如果枚举类需要的是无参构造,就不需要声明,写常量对象列表时也不用加参数
  • 如果枚举类需要的是有参构造,需要手动定义,有参构造的private可以省略,调用有参构造的方法就是在常量对象名后面加(实参列表)就可以。
  • 枚举类默认继承的是java.lang.Enum类,因此不能再继承其他的类型。
  • JDK5.0 之后switch,提供支持枚举类型,case后面可以写枚举常量名,无需添加枚举类作为限定。

举例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public enum SeasonEnum {
SPRING("春天","春风又绿江南岸"),
SUMMER("夏天","映日荷花别样红"),
AUTUMN("秋天","秋水共长天一色"),
WINTER("冬天","窗含西岭千秋雪");

private final String seasonName;
private final String seasonDesc;

private SeasonEnum(String seasonName, String seasonDesc) {
this.seasonName = seasonName;
this.seasonDesc = seasonDesc;
}
public String getSeasonName() {
return seasonName;
}
public String getSeasonDesc() {
return seasonDesc;
}
}

举例3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public enum Week {
MONDAY("星期一"),
TUESDAY("星期二"),
WEDNESDAY("星期三"),
THURSDAY("星期四"),
FRIDAY("星期五"),
SATURDAY("星期六"),
SUNDAY("星期日");

private final String description;

private Week(String description){
this.description = description;
}

@Override
public String toString() {
return super.toString() +":"+ description;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class TestWeek {
public static void main(String[] args) {
Week week = Week.MONDAY;
System.out.println(week);

switch (week){
case MONDAY:
System.out.println("怀念周末,困意很浓");break;
case TUESDAY:
System.out.println("进入学习状态");break;
case WEDNESDAY:
System.out.println("死撑");break;
case THURSDAY:
System.out.println("小放松");break;
case FRIDAY:
System.out.println("又信心满满");break;
case SATURDAY:
System.out.println("开始盼周末,无心学习");break;
case SUNDAY:
System.out.println("一觉到下午");break;
}
}
}

经验之谈:

开发中,当需要定义一组常量时,强烈建议使用枚举类。

9.4 enum中常用方法

1
2
3
4
5
6
7
8
9
1. String toString(): 默认返回的是常量名(对象名),可以继续手动重写该方法!

2. static 枚举类型[] values():返回枚举类型的对象数组。该方法可以很方便地遍历所有的枚举值,是一个静态方法

3. static 枚举类型 valueOf(String name):可以把一个字符串转为对应的枚举类对象。要求字符串必须是枚举类对象的“名字”。如不是,会有运行时异常:IllegalArgumentException。

4. String name():得到当前枚举常量的名称。建议优先使用toString()。

5. int ordinal():返回当前枚举常量的次序号,默认从0开始

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class SeasonTest1 {
public static void main(String[] args) {
//1.toString() ---返回常量名(对象名)
System.out.println(Season1.SPRING);
System.out.println(Season1.AUTUMN);
System.out.println("---------");

//2.name() ---得到当前枚举常量的名称 [建议先用toString()[
System.out.println(Season1.AUTUMN.name());
System.out.println("---------");

//静态方法
//3.values() ---返回枚举类型的对象数组,可以方便遍历所有的枚举值
Season1[] values=Season1.values();
for (int i = 0; i < values.length; i++) {
System.out.println(values[i]);
}
System.out.println("---------");

//4.valueOf(String name) ---一个字符串转为对应的枚举类对象 [要求字符串必须是枚举类对象的"名字"]
String objName="WINTER";
Season1 s1=Season1.valueOf(objName);
System.out.println(s1);
System.out.println("---------");

//5.ordinal() ---返回当前枚举常量的次序号[从0开始]
System.out.println(Season1.AUTUMN.ordinal());

}
}

enum Season1{
//1.开头必须声明多个对象 之间用逗号隔开
SPRING("春天", "春暖花开"),
SUMMER("夏天", "夏日炎炎"),
AUTUMN("秋天", "秋高气爽"),
WINTER("冬天", "白雪皑皑");
//1.私有化类的构造器
private Season1(String seasonName, String seasoonDesc){
this.seasonName = seasonName;
this.seasoonDesc = seasoonDesc;
}
//2.声明当前类的对象的实例变量 --只让他获取(private)但是不能修改(没有set方法) 【get方法/构造器内赋值】
private final String seasonName; //季节的名称
private final String seasoonDesc; //季节的描述
//3.提供get方法
public String getSeasonName() {
return seasonName;
}
public String getSeasoonDesc() {
return seasoonDesc;
}
}
最终输出:
SPRING
AUTUMN
---------
AUTUMN
---------
SPRING
SUMMER
AUTUMN
WINTER
---------
WINTER
---------
2

9.5 实现接口的枚举类

  • 和普通 Java 类一样,枚举类可以实现一个/多个接口
  • 若每个枚举值呈现相同的行为方式,统一实现该方法。
  • 若每个枚举值呈现出不同的行为方式,让每个枚举值分别实现该方法

语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//1、枚举类可以像普通的类一样,实现接口,并且可以多个,但要求必须实现里面所有的抽象方法!
enum A implements 接口1,接口2{
//抽象方法的实现
}

//2、如果枚举类的常量可以继续重写抽象方法!
enum A implements 接口1,接口2{
常量名1(参数){
//抽象方法的实现或重写
},
常量名2(参数){
//抽象方法的实现或重写
},
//...
}

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
interface Info{
public abstract void show(); //抽象方法
}

enum Season1 implements Info{
//1.开头必须声明多个对象 之间用逗号隔开
SPRING("春天", "春暖花开"){
@Override
public void show(){
System.out.println("春天在哪里?");
}
},
SUMMER("夏天", "夏日炎炎"){
@Override
public void show(){
System.out.println("春天在哪里?");
}
},
AUTUMN("秋天", "秋高气爽"){
@Override
public void show(){
System.out.println("春天在哪里?");
}
},
WINTER("冬天", "白雪皑皑"){
@Override
public void show(){
System.out.println("春天在哪里?");
}
};

//1.私有化类的构造器
private Season1(String seasonName, String seasoonDesc){
this.seasonName = seasonName;
this.seasoonDesc = seasoonDesc;
}

//2.声明当前类的对象的实例变量 --只让他获取(private)但是不能修改(没有set方法) 【get方法/构造器内赋值】
private final String seasonName; //季节的名称
private final String seasoonDesc; //季节的描述

//3.提供get方法
public String getSeasonName() {
return seasonName;
}
public String getSeasoonDesc() {
return seasoonDesc;
}

@Override
public void show(){
System.out.println("统一的重写");
}

}

10. 注解(Annotation)

10.1 注解概述

10.1.1 什么是注解

注解(Annotation)是从JDK5.0开始引入,以“@注解名”在代码中存在。例如:

1
@Override
1
@Deprecated
1
@SuppressWarnings(value=”unchecked”)

Annotation 可以像修饰符一样被使用,
①修饰包、类、构造器、方法、成员变量、参数、局部变量的声明、
②参数值。这些信息被保存在注解的 “name=value” 中。

注解可以在类编译、运行时进行加载,体现不同的功能。

10.1.2 注解与注释

注解也可以看做是一种注释,通过使用 Annotation,程序员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充信息。但是,注解,不同于单行注释和多行注释。

  • 对于单行注释和多行注释是给程序员看的。
  • 而注解是可以被编译器或其他程序读取的。程序还可以根据注解的不同,做出相应的处理。

10.1.3 注解的重要性

在JavaSE中,注解的使用目的比较简单,例如标记过时的功能,忽略警告等。在JavaEE/Android中注解占据了更重要的角色,例如用来配置应用程序的任何切面,代替JavaEE旧版中所遗留的繁冗代码XML配置等。

未来的开发模式都是基于注解的,JPA是基于注解的,Spring2.5以上都是基于注解的,Hibernate3.x以后也是基于注解的,Struts2有一部分也是基于注解的了。注解是一种趋势,一定程度上可以说:框架 = 注解 + 反射 + 设计模式

10.2 常见的Annotation作用

示例1:生成文档相关的注解

1
2
3
4
5
6
7
@author 标明开发该类模块的作者,多个作者之间使用,分割
@version 标明该类模块的版本
@see 参考转向,也就是相关主题
@since 从哪个版本开始增加的
@param 对方法中某参数的说明,如果没有参数就不能写
@return 对方法返回值的说明,如果方法的返回值类型是void就不能写
@exception 对方法可能抛出的异常进行说明 ,如果方法没有用throws显式抛出的异常就不能写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.annotation.javadoc;
/**
* @author 尚硅谷-宋红康
* @version 1.0
* @see Math.java
*/
public class JavadocTest {
/**
* 程序的主方法,程序的入口
* @param args String[] 命令行参数
*/
public static void main(String[] args) {
}

/**
* 求圆面积的方法
* @param radius double 半径值
* @return double 圆的面积
*/
public static double getArea(double radius){
return Math.PI * radius * radius;
}
}

示例2:在编译时进行格式检查(JDK内置的三个基本注解)

@Override: 限定重写父类方法,该注解只能用于方法

@Deprecated: 用于表示所修饰的元素(类,方法等)已过时。通常是因为所修饰的结构危险或存在更好的选择

@SuppressWarnings: 抑制编译器警告

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.annotation.javadoc;
 
public class AnnotationTest{
 
public static void main(String[] args) {
@SuppressWarnings("unused")
int a = 10;
}
@Deprecated
public void print(){
System.out.println("过时的方法");
}
 
@Override
public String toString() {
return "重写的toString方法()";
}
}

示例3:跟踪代码依赖性,实现替代配置文件功能

  • Servlet3.0提供了注解(annotation),使得不再需要在web.xml文件中进行Servlet的部署。
1
2
3
4
5
6
7
8
9
10
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
private static final long serialVersionUID = 1L;

protected void doGet(HttpServletRequest request, HttpServletResponse response) { }
 
protected void doPost(HttpServletRequest request, HttpServletResponse response) {
doGet(request, response);
}
}
1
2
3
4
5
6
7
8
<servlet>
<servlet-name>LoginServlet</servlet-name>
<servlet-class>com.servlet.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>LoginServlet</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>
  • Spring框架中关于“事务”的管理
1
2
3
4
5
6
7
8
9
@Transactional(propagation=Propagation.REQUIRES_NEW,isolation=Isolation.READ_COMMITTED,readOnly=false,timeout=3)
public void buyBook(String username, String isbn) {
//1.查询书的单价
int price = bookShopDao.findBookPriceByIsbn(isbn);
//2. 更新库存
bookShopDao.updateBookStock(isbn);
//3. 更新用户的余额
bookShopDao.updateUserAccount(username, price);
}
1
2
3
4
5
6
7
8
<!-- 配置事务属性 -->
<tx:advice transaction-manager="dataSourceTransactionManager" id="txAdvice">
<tx:attributes>
<!-- 配置每个方法使用的事务属性 -->
<tx:method name="buyBook" propagation="REQUIRES_NEW"
isolation="READ_COMMITTED" read-only="false" timeout="3" />
</tx:attributes>
</tx:advice>

10.3 三个最基本的注解

10.3.1 @Override

  • 用于检测被标记的方法为有效的重写方法,如果不是,则报编译错误!
  • 只能标记在方法上。
  • 它会被编译器程序读取。

10.3.2 @Deprecated

  • 用于表示被标记的数据已经过时,不推荐使用。
  • 可以用于修饰 属性、方法、构造、类、包、局部变量、参数。
  • 它会被编译器程序读取。

10.3.3 @SuppressWarnings

  • 抑制编译警告。当我们不希望看到警告信息的时候,可以使用 SuppressWarnings 注解来抑制警告信息
  • 可以用于修饰类、属性、方法、构造、局部变量、参数
  • 它会被编译器程序读取。

  • 可以指定的警告类型有(了解)

    • all,抑制所有警告
    • unchecked,抑制与未检查的作业相关的警告
    • unused,抑制与未用的程式码及停用的程式码相关的警告
    • deprecation,抑制与淘汰的相关警告
    • nls,抑制与非 nls 字串文字相关的警告
    • null,抑制与空值分析相关的警告
    • rawtypes,抑制与使用 raw 类型相关的警告
    • static-access,抑制与静态存取不正确相关的警告
    • static-method,抑制与可能宣告为 static 的方法相关的警告
    • super,抑制与置换方法相关但不含 super 呼叫的警告

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.util.ArrayList;
public class TestAnnotation {
@SuppressWarnings("all")
public static void main(String[] args) {
int i;

ArrayList list = new ArrayList();
list.add("hello");
list.add(123);
list.add("world");

Father f = new Son();
f.show();
f.methodOl();
}
}

class Father{
@Deprecated
void show() {
System.out.println("Father.show");
}
void methodOl() {
System.out.println("Father Method");
}
}

class Son extends Father{
/* @Override
void method01() {
System.out.println("Son Method");
}*/
}

10.4 元注解

元注解:对现有的注解进行解释说明的注解。

JDK1.5在java.lang.annotation包定义了4个标准的meta-annotation类型,它们被用来提供对其它 annotation类型作说明。

(1)@Target:用于描述注解的使用范围

  • 可以通过枚举类型ElementType的10个常量对象来指定
  • TYPE,METHOD,CONSTRUCTOR,PACKAGE…..

(2)@Retention:用于描述注解的生命周期

  • 可以通过枚举类型RetentionPolicy的3个常量对象来指定
  • SOURCE(源代码)、CLASS(字节码)、RUNTIME(运行时)
  • 唯有RUNTIME阶段才能被反射读取到

(3)@Documented:表明这个注解应该被 javadoc工具记录。

(4)@Inherited:允许子类继承父类中的注解

示例代码:

1
2
3
4
5
6
7
8
package java.lang;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
1
2
3
4
5
6
7
8
9
10
package java.lang;

import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
1
2
3
4
5
6
7
8
9
10
package java.lang;

import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}

拓展:元数据

String name = “Tom”;

10.5 自定义注解的使用

一个完整的注解应该包含三个部分:
(1)声明
(2)使用
(3)读取

10.5.1 声明自定义注解

1
2
3
4
【元注解】
【修饰符】 @interface 注解名{
【成员列表】
}
  • 自定义注解可以通过四个元注解@Retention,@Target,@Inherited,@Documented,分别说明它的声明周期,使用位置,是否被继承,是否被生成到API文档中。
  • Annotation 的成员在 Annotation 定义中以无参数有返回值的抽象方法的形式来声明,我们又称为配置参数。返回值类型只能是八种基本数据类型、String类型、Class类型、enum类型、Annotation类型、以上所有类型的数组。
  • 可以使用 default 关键字为抽象方法指定默认返回值
  • 如果定义的注解含有抽象方法,那么使用时必须指定返回值,除非它有默认值。格式是“方法名 = 返回值”,如果只有一个抽象方法需要赋值,且方法名为value,可以省略“value=”,所以如果注解只有一个抽象方法成员,建议使用方法名value。
1
2
3
4
5
6
7
import java.lang.annotation.*;
@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
String value();
}
1
2
3
4
5
6
7
8
import java.lang.annotation.*;
@Inherited
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
String columnName();
String columnType();
}

10.5.2 使用自定义注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.atguigu.annotation;

@Table("t_stu")
public class Student {
@Column(columnName = "sid",columnType = "int")
private int id;
@Column(columnName = "sname",columnType = "varchar(20)")
private String name;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

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

@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}

10.5.3 读取和处理自定义注解

自定义注解必须配上注解的信息处理流程才有意义。

我们自己定义的注解,只能使用反射的代码读取。所以自定义注解的声明周期必须是RetentionPolicy.RUNTIME。

具体的使用见《尚硅谷_宋红康_第17章_反射机制.md》

10.6 JUnit单元测试

10.6.1 测试分类

黑盒测试:不需要写代码,给输入值,看程序是否能够输出期望的值。

白盒测试:需要写代码的。关注程序具体的执行流程。

image-20220511181800694

image-20220524102038600

10.6.2 JUnit单元测试介绍

JUnit 是由 Erich Gamma 和 Kent Beck 编写的一个测试框架(regression testing framework),供Java开发人员编写单元测试之用。

JUnit测试是程序员测试,即所谓白盒测试,因为程序员知道被测试的软件如何(How)完成功能和完成什么样(What)的功能。

要使用JUnit,必须在项目的编译路径中引入JUnit的库,即相关的.class文件组成的jar包。jar就是一个压缩包,压缩包都是开发好的第三方(Oracle公司第一方,我们自己第二方,其他都是第三方)工具类,都是以class文件形式存在的。

10.6.3 引入本地JUnit.jar

第1步:在项目中File-Project Structure中操作:添加Libraries库

image-20211228180938922

image-20221002195547325

其中,junit-libs包内容如下:

image-20220813005206452

第2步:选择要在哪些module中应用JUnit库

image-20220813005511062

第3步:检查是否应用成功

image-20220813005729233

注意Scope:选择Compile,否则编译时,无法使用JUnit。

第4步:下次如果有新的模块要使用该libs库,这样操作即可

image-20220813005944022

image-20220813010018152

image-20220813010055217

image-20220813010124381

10.6.4 编写和运行@Test单元测试方法

JUnit4版本,要求@Test标记的方法必须满足如下要求:

  • 所在的类必须是public的,非抽象的,包含唯一的无参构造器。
  • @Test标记的方法本身必须是public,非抽象的,非静态的,void无返回值,()无参数的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.junit.Test;
public class TestJUnit {
@Test
public void test01(){
System.out.println("TestJUnit.test01");
}

@Test
public void test02(){
System.out.println("TestJUnit.test02");
}

@Test
public void test03(){
System.out.println("TestJUnit.test03");
}
}

image-20220106152412245

10.6.5 设置执行JUnit用例时支持控制台输入

1. 设置数据:

默认情况下,在单元测试方法中使用Scanner时,并不能实现控制台数据的输入。需要做如下设置:

idea64.exe.vmoptions配置文件中加入下面一行设置,重启idea后生效。

1
-Deditable.java.test.console=true

2. 配置文件位置:

image-20220813011625546

image-20220813011642180

添加完成之后,重启IDEA即可。

3. 如果上述位置设置不成功,需要继续修改如下位置

修改位置1:IDEA安装目录的bin目录(例如:D:\develop_tools\IDEA\IntelliJ IDEA 2022.1.2\bin)下的idea64.exe.vmoptions文件。

修改位置2:C盘的用户目录C:\Users\用户名\AppData\Roaming\JetBrains\IntelliJIdea2022.1 下的idea64.exe.vmoptions`件。

10.6.6 定义test测试方法模板

选中自定义的模板组,点击”+”(1.Live Template)来定义模板。

image-20211229100040505

11. 包装类

11.1 为什么需要包装类

Java提供了两个类型系统,基本数据类型引用数据类型。使用基本数据类型在于效率,然而当要使用只针对对象设计的API或新特性(例如泛型),怎么办呢?例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//情况1:方法形参
Object类的equals(Object obj)

//情况2:方法形参
ArrayList类的add(Object obj)
//没有如下的方法:
add(int number)
add(double d)
add(boolean b)

//情况3:泛型
Set<T>
List<T>
Cllection<T>
Map<K,V>

11.2 有哪些包装类

Java针对八种基本数据类型定义了相应的引用类型:包装类(封装类)。有了类的特点,就可以调用类中的方法,Java才是真正的面向对象。

image-20220329001912486

封装以后的,内存结构对比:

1
2
3
4
public static void main(String[] args){
int num = 520;
Integer obj = new Integer(520);
}
image-20220514163725830

11.3 自定义包装类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyInteger {
//属性
int value;
//无参构造器
public MyInteger() {
}
//有参构造器
public MyInteger(int value) {
this.value = value;
}
//重写toString方法
@Override
public String toString() {
return String.valueOf(value); //返回valueOf方法的值
}
}

11.4 包装类与基本数据类型间的转换

11.4.1 装箱

装箱:把基本数据类型转为包装类对象

转为包装类的对象,是为了使用专门为对象设计的API和特性

基本数值—->包装对象

1
2
3
4
5
Integer obj1 = new Integer(4);//使用构造函数函数
Float f = new Float(“4.56”);
Long l = new Long(“asdf”); //NumberFormatException

Integer obj2 = Integer.valueOf(4);//使用包装类中的valueOf方法

11.4.2 拆箱

拆箱:把包装类对象拆为基本数据类型

转为基本数据类型,一般是因为需要运算,Java中的大多数运算符是为基本数据类型设计的。比较、算术等

包装对象—->基本数值

1
2
Integer obj = new Integer(4);
int num1 = obj.intValue();

自动装箱与拆箱:

由于我们经常要做基本类型与包装类之间的转换,从JDK5.0开始,基本类型与包装类的装箱、拆箱动作可以自动完成。例如:

1
2
3
Integer i = 4;//自动装箱。相当于Integer i = Integer.valueOf(4);
i = i + 5;//等号右边:将i对象转成基本数值(自动拆箱) i.intValue() + 5;
//加法运算完成后,再次装箱,把基本数值转成对象。

注意:只能与自己对应的类型之间才能实现自动装箱与拆箱。

1
2
Integer i = 1;
Double d = 1;//错误的,1是int类型

11.5 基本数据类型、包装类与字符串间的转换

(1)基本数据类型转为字符串

方式1:调用字符串重载的valueOf()方法

1
2
3
4
int a = 10;
//String str = a;//错误的

String str = String.valueOf(a);

方式2:更直接的方式

1
2
3
int a = 10;

String str = a + "";

(2)字符串转为基本数据类型

方式1:除了Character类之外,其他所有包装类都具有parseXxx静态方法可以将字符串参数转换为对应的基本类型,例如:

  • public static int parseInt(String s):将字符串参数转换为对应的int基本类型。
  • public static long parseLong(String s):将字符串参数转换为对应的long基本类型。
  • public static double parseDouble(String s):将字符串参数转换为对应的double基本类型。

方式2:字符串转为包装类,然后可以自动拆箱为基本数据类型

  • public static Integer valueOf(String s):将字符串参数转换为对应的Integer包装类,然后可以自动拆箱为int基本类型
  • public static Long valueOf(String s):将字符串参数转换为对应的Long包装类,然后可以自动拆箱为long基本类型
  • public static Double valueOf(String s):将字符串参数转换为对应的Double包装类,然后可以自动拆箱为double基本类型

注意:如果字符串参数的内容无法正确转换为对应的基本类型,则会抛出java.lang.NumberFormatException异常。

方式3:通过包装类的构造器实现

1
2
3
4
5
6
7
8
9
int a = Integer.parseInt("整数的字符串");
double d = Double.parseDouble("小数的字符串");
boolean b = Boolean.parseBoolean("true或false");

int a = Integer.valueOf("整数的字符串");
double d = Double.valueOf("小数的字符串");
boolean b = Boolean.valueOf("true或false");

int i = new Integer(“12”);

其他方式小结:

image-20220813012801907

11.6 包装类的其它API

11.6.1 数据类型的最大最小值

1
2
3
4
5
Integer.MAX_VALUE和Integer.MIN_VALUE

Long.MAX_VALUE和Long.MIN_VALUE

Double.MAX_VALUE和Double.MIN_VALUE

11.6.2 字符转大小写

1
2
3
Character.toUpperCase('x');

Character.toLowerCase('X');

11.6.3 整数转进制

1
2
3
4
5
Integer.toBinaryString(int i) 

Integer.toHexString(int i)

Integer.toOctalString(int i)

11.6.4 比较的方法

1
2
3
Double.compare(double d1, double d2)

Integer.compare(int x, int y)

11.7 包装类对象的特点

11.7.1 包装类缓存对象

包装类 缓存对象
Byte -128~127
Short -128~127
Integer -128~127
Long -128~127
Float 没有
Double 没有
Character 0~127
Boolean true和false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Integer a = 1;
Integer b = 1;
System.out.println(a == b);//true

Integer i = 128;
Integer j = 128;
System.out.println(i == j);//false

Integer m = new Integer(1);//新new的在堆中
Integer n = 1;//这个用的是缓冲的常量对象,在方法区
System.out.println(m == n);//false

Integer x = new Integer(1);//新new的在堆中
Integer y = new Integer(1);//另一个新new的在堆中
System.out.println(x == y);//false
1
2
3
Double d1 = 1.0;
Double d2 = 1.0;
System.out.println(d1==d2);//false 比较地址,没有缓存对象,每一个都是新new的

11.7.2 类型转换问题

1
2
3
Integer i = 1000;  //先自动拆箱为int  然后自动类型转换为double
double j = 1000;
System.out.println(i==j);//true 会先将i自动拆箱为int,然后根据基本数据类型“自动类型转换”规则,转为double比较
1
2
3
Integer i = 1000;  //先自动拆箱为int
int j = 1000;
System.out.println(i==j);//true 会自动拆箱,按照基本数据类型进行比较
1
2
3
Integer i = 1;  //先自动拆箱为int
Double d = 1.0 //没写;
System.out.println(i==d);//编译报错

11.7.3 包装类对象不可变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class TestExam {
public static void main(String[] args) {
int i = 1;
Integer j = new Integer(2);
Circle c = new Circle();
change(i,j,c);
System.out.println("i = " + i);//1
System.out.println("j = " + j);//2
System.out.println("c.radius = " + c.radius);//10.0
}

/*
* 方法的参数传递机制:
* (1)基本数据类型:形参的修改完全不影响实参
* (2)引用数据类型:通过形参修改对象的属性值,会影响实参的属性值
* 这类Integer等包装类对象是“不可变”对象,即一旦修改,就是新对象,和实参就无关了
*/
public static void change(int a ,Integer b,Circle c ){
a += 10;
// b += 10;//等价于 b = new Integer(b+10);
c.radius += 10;
/*c = new Circle();
c.radius+=10;*/
}
}
class Circle{
double radius;
}

11.8 练习

笔试题:如下两个题目输出结果相同吗?各是什么。

1
2
Object o1 = true ? new Integer(1) : new Double(2.0);  //在创建的时候要进行统一编译 同时自动类型转换到1.0和2.0
System.out.println(o1);//1.0 相当于创建两个包装类对象
1
2
3
4
5
6
Object o2;
if (true)
o2 = new Integer(1);
else
o2 = new Double(2.0);
System.out.println(o2); //输出1

面试题:

1
2
3
4
5
6
7
8
9
10
11
12
13
Integer i = new Integer(1);
Integer j = new Integer(1);
System.out.println(i == j); //输出false

Integer m = 1;
Integer n = 1;
System.out.println(m == n); //输出true

// Integer内部定义了一个IntegerCache结构,IntegerCache中定义Integer[],保存-128-127范围内的整数。
// 如果我们使用自动装箱的方式【Integer i=xxx; xxx属于(-128,127]】,就可以直接使用数组中的元素。
Integer x = 128; //128相当于重新new了一个
Integer y = 128; //128相当于重新new了一个
System.out.println(x == y);//输出false

练习:

利用Vector代替数组处理:从键盘读入学生成绩(以负数代表输入结束),找出最高分,并输出学生成绩等级。

  • 提示:数组一旦创建,长度就固定不变,所以在创建数组前就需要知道它的长度。而向量类java.util.Vector可以根据需要动态伸缩。

  • 创建Vector对象:Vector v=new Vector();

  • 给向量添加元素:v.addElement(Object obj); //obj必须是对象

  • 取出向量中的元素:Object obj=v.elementAt(0);

    • 注意第一个元素的下标是0,返回值是Object类型的。
  • 计算向量的长度:v.size();

  • 若与最高分相差10分内:A等;20分内:B等;30分内:C等;其它:D等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import java.util.Scanner;
import java.util.Vector;
public class WrapperTest3 {
public static void main(String[] args) {
Vector vector=new Vector();
Scanner input=new Scanner(System.in);
for(;;){
double x=input.nextDouble();
if(x<0){
break;
}else{
vector.addElement(x); //添加向量
}
}
System.out.println("向量长度:"+vector.size());
Vector vector1=new Vector();
double max=Integer.MIN_VALUE; //寻找最大值
for(int i=0;i<vector.size();i++){
Object obj=vector.elementAt(i); //取出当前值
vector1.add(obj); //到时候判断用
double temp= (double) obj; //转换为double值
max=(max>temp?max:temp); //替换max
}
System.out.println("最大值:"+max);
for(int i=0;i<vector1.size();i++){
double cha=max-(double)vector1.elementAt(i); //要统一为double
if(cha<10){
System.out.println("A");
}else if(cha<20){
System.out.println("B");
}else if(cha<30){
System.out.println("C");
}else{
System.out.println("D");
}
}

}
}

最终结果如下:
image-20231021185053363

JAVA-基础篇

第01章_Java语言概述


1. Java知识脉络图

1.1 Java基础全程脉络图

Java基础全程脉络图

1.2 本章专题与脉络

第1阶段:Java基本语法-第01章

2. 抽丝剥茧话Java

2.1 当前大学生就业形势

  • 麦可思研究院发布了《2022年中国大学生就业报告》,针对2021届毕业生收入较高的本科专业排行榜:
img
  • 麦可思研究院发布过《2021年中国大学生就业报告》,应届本科毕业生就业数量较大的前十位行业类的就业质量:

    jiuyezhiliang1
  • 报告还对毕业三年后的2017届毕业生所在十大行业进行了统计:

jiuyezhiliang2
  • 从国家统计局发布的2021年全国平均工资来看,不管在城镇非私营单位还是私营单位,IT业均为最高

2.2 IT互联网是否依旧靠谱

过去不能代表未来!互联网是否依旧靠谱?!

image-20220523002428542

2014 年至 2018 年间,我国网民规模从 6.49 亿增长为 8.29 亿,增幅为 27.5%。同一时间段,全国移动互联网接入的流量却从 20.6EB 增长到了 711.1EB,增幅达 3352%(获取和处理的信息量大幅增加)。

随着 5G 技术进一步拓宽移动互联网的速度和容量,产业互联网将在消费型流量的基础上创造生产型流量,根据报告的预测,至 2024 年,全国移动互联网的接入流量将达到 127663.8EB, 流量规模达到2018年的179.5倍

当下,5G、物联网、人工智能、产业互联网都是国家政策大方向,需要大量能与机器对话的中高端人才。

2.3 IT行业岗位分析

image-20220522234544537

软件开发,是进入互联网IT圈最好的选择之一!

  • 起始薪资高
  • 工作环境好
  • 涨薪幅度高
  • 行业更公平

2.4 软件开发之Java开发

image-20220504101615999

image-20220608160444947

  • 移动应用领域(集成Android平台):Java在Android端是主要开发的语言,占有重要的地位。

    image-20220511092649122 20220608_160310
  • 企业级应用领域(JavaEE后台):用来开发企业级的应用程序,大型网站如淘宝、京东、12306,以及各大物流、银行、金融、社交、医疗、交通、各种OA系统等都是用JavaEE技术开发的。

    image-20220511090528043
  • 大数据分析、人工智能领域:流行的大数据框架,如Hadoop、Flink都是用Java编写的。Spark使用Scala编写,但可以用Java开发应用。

    image-20220511091647358

    image-20220513233529610

    Eversoft公司在提到2022年Java发展趋势时写道:

    Java 是用于开发大数据项目的最主流的语言。我们可以轻松地预测它也将在之后继续主导大数据

  • 游戏领域、桌面应用、嵌入式领域:很多大型游戏的后台、桌面应用等也是Java开发的。

2.5 到底多少人在用Java

2020年,根据 IDC 的报告“Java Turns 25”显示,超过 900 万名开发人员(全球专职开发人员中的 69%)在使用 Java——比其他任何语言都多。该报告指出,大多数企业业务服务都依靠 Java 来实现。

… Java 一直是开发人员中最流行的编程语言,被誉为“宇宙第一语言”。

我想告诉大家:

“市场的需求比较大,市场的供给比较大”

“如果你在Java领域里持续积累5-7年以上,那么你至少会成为这个行业的一个专家!”

2.6 八卦一下程序员

image-20220602105134523

还可以是:

image-20220602105213557

2.7 Java系列课程体系

  • 见02_学习路线图之《Java中高级程序员全程学习路线图.xmind》

2.8 Java职业晋升路线图

技术发展路线

薪资数据统计来源:拉勾网

3. 计算机的硬件与软件

3.1 计算机组成:硬件+软件

image-20220524152902812

3.2 CPU、内存与硬盘

  • CPU(Central Processing Unit,中央处理器)

    • 人靠大脑思考,电脑靠CPU来运算、控制。
  • 硬盘(Hard Disk Drive)

    • 计算机最主要的存储设备,容量大,断电数据不丢失。
    • 正常分类:机械硬盘(HDD)固态硬盘(SSD)以及混合硬盘(SSHD)
    • 固态硬盘在开机速度和程序加载速度远远高于机械硬盘,缺点就是贵,所有无法完全取代机械硬盘。
  • 内存(Memory)

    • 负责硬盘上的数据与CPU之间数据交换处理
    • 具体的:保存从硬盘读取的数据,提供给CPU使用;保存CPU的一些临时执行结果,以便CPU下次使用或保存到硬盘。
    • 断电后数据丢失。

image-20220731234604511

3.3 输入设备:键盘输入

  • 熟悉指法
image-20220517100223784

不熟悉键盘的小伙伴,可以“金山打字通”走起了。坚决杜绝二指禅!!

image-20220517100543176

4. 软件相关介绍

4.1 什么是软件

软件,即一系列按照特定顺序组织的计算机数据指令的集合。有系统软件应用软件之分。

Pascal之父Nicklaus Wirth: “Programs = Data Structures + Algorithms”

系统软件:

image-20220522215226273

应用软件:

macdesk

4.2 人机交互方式

  • 图形化界面(Graphical User Interface,GUI),这种方式简单直观,使用者易于接受,容易上手操作。

  • 命令行方式(Command Line Interface,CLI),需要在控制台输入特定的指令,让计算机完成一些操作。需要记忆一些指令,较为麻烦。

    img

4.3 常用的DOS命令

DOS(Disk Operating System,磁盘操作系统)是Microsoft公司在Windows之前推出的一个操作系统,是单用户、单任务(即只能执行一个任务)的操作系统。现在被Windows系统取代。

对于Java初学者,学习一些DOS命令,会非常有帮助。

进入DOS操作窗口:

  • 按下Windows+R键盘,打开运行窗口,输入cmd回车,进入到DOS的操作窗口。

    image-20220520100110104

常用指令:

  • 操作1:进入和回退
操作 说明
盘符名称: 盘符切换。E:回车,表示切换到E盘。
dir 列出当前目录下的文件以及文件夹
cd 目录 进入指定单级目录。
cd 目录1\目录2\… 进入指定多级目录。cd atguigu\JavaSE
cd .. 回退到上一级目录。
cd \ 或 cd / 回退到盘符目录。
  • 操作2:增、删
操作 说明
md 文件目录名 创建指定的文件目录。
rd 文件目录名 删除指定的文件目录(如文件目录内有数据,删除失败)
  • 操作3:其它
操作 说明
cls 清屏。
exit 退出命令提示符窗口。
← → 移动光标
↑ ↓ 调阅历史操作命令
Delete和Backspace 删除字符

5. 计算机编程语言

5.1 计算机语言是什么

  • 语言:是人与人之间用于沟通的一种方式。例如:中国人与中国人用普通话沟通。而中国人要和英国人交流,可以使用英语或普通话。

  • 计算机编程语言,就是人与计算机交流的方式。人们可以使用编程语言对计算机下达命令,让计算机完成人们需要的功能。

  • 计算机语言有很多种。如:C 、C++、Java、Go、JavaScript、Python,Scala等。

体会:语言 = 语法 + 逻辑

5.2 计算机语言简史

  • 第一代:机器语言(相当于人类的石器时代)

    • 1946年2月14日,世界上第一台计算机ENAC诞生,使用的是最原始的穿孔卡片。这种卡片上使用的是用二进制代码表示的语言,与人类语言差别极大,这种语言就称为机器语言。比如一段典型的机器码:

      1
      2
      3
      1.	0000,0000,000000010000 代表 LOAD A, 16
      2. 0000,0001,000000000001 代表 LOAD B, 1
      3. 0001,0001,000000010000 代表 STORE B, 16
    • 这种语言本质上是计算机能识别的唯一语言,人类很难理解。可以大胆想象”此时的程序员99.9%都是异类!

      image-20220309223406537

  • 第二代:汇编语言(相当于人类的青铜&铁器时代)

    • 使用英文缩写的助记符来表示基本的操作,这些助记符构成了汇编语言的基础。比如:LOADMOVE等,使人更容易使用。因此,汇编语言也称为符号语言

    • 优点:能编写高效率的程序

    • 缺点:汇编语言是面向机器的,不同计算机机型特点不同,因此会有不同的汇编语言,彼此之间不能通用。程序不易移植,较难调试。

      image-20220309223725671
    • 比起机器语言,汇编大大进步了,是机器语言向更高级的语言进化的桥梁。目前仍然应用于工业电子编程领域、软件的加密解密、计算机病毒分析等。

  • 第三代:高级语言(相当于人类的信息时代)

    • 高级语言发展于20世纪50年代中叶到70年代,是一种接近于人们使用习惯的程序设计语言。它允许程序员使用接近日常英语的指令来编写程序,程序中的符号和算式也与日常用的数学式子差不多,接近于自然语言和数学语言,容易为人们掌握。比如:

      image-20211218092541175

    • 高级语言独立于机器,有一定的通用性;计算机不能直接识别和执行用高级语言编写的程序,需要使用编译器或者解释器,转换为机器语言才能被识别和执行。

image-20211218092630678

image-20211218092541075

此外,高级语言按照程序设计方法的不同,又分为:面向过程的语言面向对象的语言

  • C、Pascal、Fortran面向过程的语言
  • C++面向过程/面向对象
  • Java跨平台的纯面向对象的语言
  • C#、Python、JavaScript、Scala…

目前以及可预见的将来,计算机语言仍然处于“第三代高级语言”阶段。但是不管是什么语言,最后都要向机器语言靠近,因为CPU只认识0和1。

5.3 计算机语言排行榜

TIOBE (https://www.tiobe.com/tiobe-index/)是一个流行编程语言排行,每月更新。排名权重基于世界范围内 工程师数量,Google、Bing、Yahoo! 、Wikipedia、Amazon、Youtube和百度这些主流的搜索引擎,也将作为排名权重的参考指标。

image-20221027112824724

计算机语言走势

image-20221027113511832

5.4 编程语言,该学哪个?

image-20220310151657860

网传的编程语言鄙视链:

image-20220310151750665

  • C语言:万物之源
  • 诞生于1972年,由AT&T公司旗下贝尔实验室创建完成,用于构建Unix操作系统。

  • 偏向计算机底层操作(操作系统、网络、硬件驱动等)。

  • 优势:几乎所有现代编程语言都脱胎于C

  • 劣势:相当陡的学习曲线;不支持面向对象编程

  • C++语言:难学的语言
  • 诞生于1983年,作为C语言的增强方案、升级版本。C++是C语言的超集,C语言的大部分知识也适用于C++。
  • 用途:windows 或 MacOS UI、office全家桶、主流的浏览器、Oracle、MySQL、著名的游戏引擎(如星际争霸、魔兽世界)等
  • 优势:很多公司都用 C++ 开发核心架构,如 Google、腾讯、百度、阿里云等;面向对象性
  • 劣势:体系极为庞大,这是世界目前来说最复杂也是最难的编程语言。
  • C#语言:背靠大树的语言
  • 诞生于2000年,一款强大而灵活的编程语言。靠着微软这棵大树,是多年来windows平台的一门主流编程语言。
  • 用途:windows桌面应用开发、Windows Azure、游戏开发
  • PHP语言:最好的语言?
  • 诞生于1994年,一款服务器端脚本语言。最初表示个人主页(Personal Home Page)
  • PHP语法和C类似,有很多的模板和框架,简单易懂,也许你可以在短短几天做出web app。它主要用于web端,快速建站网络开发
  • 劣势:学习门槛太低;其代码运行速度低于大部分编程语言党争对手
  • Python:易学的语言
  • 诞生于1991年,一种面向对象的语言,虽然运行效率不高,但是开发效率非常高
  • Python被称为胶水语言,哪里都可以用。
  • JavaScript语言:前端为王
  • 诞生于1995年,网景公司开发完成。
  • JavaScript是JavaScript 是目前所有主流浏览器上唯一支持的脚本语言。在前端开发中,占有不可替代的地位。
  • Java语言:需求旺盛
  • 创建于1995年,Java是一种面向对象、基于类的编程语言。
  • Java可能是目前运用最广的项目语言。代码稳定性超过 C 和 C++,生产力远超 C 和 C++。有 JVM 在,可以轻松地跨平台。
  • 具有强大的开源开发工具,大量的开源共享库。
  • Java拥有世界上数量最多的程序员,最不缺人。
  • Go语言:夹缝中生存
  • Go 语言现在很受关注,它是取代 C 和 C++ 的另一门有潜力的语言。
    • C 语言太原始了,C++ 太复杂了,Java 太高级了,所以 Go 语言就在这个夹缝中出现了。
  • Go语言已成为云计算领域事实上的标准语言,尤其是在 Docker/Kubernetes 等项目中。
  • Go 语言语法特别简单,你有了 C 和 C++ 的基础,学习 Go 的学习成本基本为零。
  • Go社区从Java社区移植了各种优秀的框架或库。

总结:

  • 程序设计语言有很多种,每种语言都是为了实现某个特定的目的而发明的。
  • 没有“最好”的语言,只有在特定场景下相对来说,最适合的语言而已。
  • 如果你掌握了一种编程语言,也会更容易上手其它的编程语言。关键是学习如何使用程序设计方法来解决问题。这也是本套课程的主旨。
  • Talk is cheap,Show me the code

6. Java语言概述

6.1 Java概述

  • SUN(Stanford University Network,斯坦福大学网络公司 ) 1995年推出的一门高级编程语言。

  • 是一种面向Internet的编程语言。Java一开始富有吸引力是因为Java程序可以在Web浏览器中运行。这些Java程序被称为Java小程序(applet),内嵌在HTML代码中。

  • 伴随着互联网的迅猛发展,以及Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。

6.2 Java语言简史

起步阶段:

1991年,Sun公司的工程师小组想要设计一种语言,应用在电视机电话闹钟烤面包机等家用电器的控制和通信。由于这些设备的处理能力内存都很有限,并且不同的厂商会选择不同的中央处理器(CPU),因此这种语言的关键是代码短小紧凑与平台无关(即不能与任何特定的体系结构捆绑在一起)。

Gosling团队率先创造了这个语言,并命名为“Oak“(起名的原因是因为他非常喜欢自己办公室外的橡树)。后因智能化家电的市场需求没有预期的高,Sun公司放弃了该项计划。

随着20世纪90年代互联网的发展,Sun公司发现该语言在互联网上应用的前景,于是改造了Oak,于1995年5月以Java的名称正式发布。(Java是印度尼西亚爪哇岛的英文名称,因盛产咖啡而闻名。)

image-20220309230839100

发展阶段:

发行版本 发行时间 备注
Java 1.0 1996.01.23 Sun公司发布了Java的第一个开发工具包
Java 1.1 1997.02.19 JavaOne会议召开,创当时全球同类会议规模之最。
Java 1.2 1998.12.08 Java拆分成:J2SE(标准版)、J2EE(企业版)、J2ME(小型版)
Java 1.3 2000.05.08
Java1.4 2004.02.06
Java 5.0 2004.09.30 ①版本号从1.4直接更新至5.0;②平台更名为JavaSE、JavaEE、JavaME
Java 6.0 2006.12.11 2009.04.20 Oracle宣布收购SUN公司
2009.04.20 Oracle公司收购SUN,交易价格74亿美元。
Java 7.0 2011.07.02
Java 8.0 2014.03.18 此版本是继Java 5.0以来变化最大的版本。是长期支持版本(LTS)
Java 9.0 2017.09.22 ①此版本开始,每半年更新一次;②Java 9.0开始不再支持windows 32位系统
Java 10.0 2018.03.21
Java 11.0 2018.09.25 JDK安装包取消独立JRE安装包,是长期支持版本(LTS)
Java 12.0 2019.03.19
Java17.0 2021.09 发布Java 17.0,版本号也称为21.9,是长期支持版本。
Java19.0 2022.09 发布Java19.0,版本号也称为22.9。

6.3 Java之父

image-20220310152923961
  • 詹姆斯·高斯林(James Gosling)先生以“Java 技术之父”而闻名于世。他是Java 技术的创始人,他亲手设计了Java语言,并开发了Java编译器和Java虚拟机,使Java成为了世界上最流行的开发语言。
  • James Gosling于1984 年加入Sun公司,并一直服务于Sun公司,直至2010年前后,Sun被Oracle并购而加入Oracle,担任客户端软件集团的首席技术官; 2010年4月从Oracle离职。

6.4 公司八卦

  • SUN 与 Oracle

SUN是一家极具创新能力的公司,2001年 “9.11”以前,SUN公司市值超过1000亿美元。 但是没能利用Java构建一个强有力、可变现的生态系统,没打好Java这张牌。此后,互联网泡沫破裂,硬件需求大幅减少,它的市值在一个月之内跌幅超过90%。SUN公司的成长用了20年,而衰落只用了1年!

image-20220309230923533
  • Oracle 与 Google

Google和Oracle的侵权事件:

2010 年 8 月,Oracle 起诉 Google 的 Android 系统侵权,要求赔偿 26 亿美元。

  • Oracle认为Google的代码中使用了Java的37个API,并且认为Google是故意为之,因为这样做的好处是可以让更多的Java程序员更容易接受Android的代码。
  • Oracle认为Android 中有9行代码直接抄袭了Java的实现。这9行牛气哄哄的代码都出自一人之手,他就是Java 大牛——Joshua Bloch

2018 年 3 月,美国联邦巡回上诉法院裁决,谷歌侵犯了甲骨文的版权,支付高达88亿美元的赔偿金。

2021 年 4 月,美国最高法院给出了最终裁决:谷歌胜诉,其代码属于“合理使用”的范畴。为期十多年的软件行业“第一版权案”落幕。

1
2
3
4
5
6
7
8
9
10
//Code In OpenJDK / Android :
1. private static void rangeCheck(int arrayLen, int fromIndex, int toIndex) {
2. if (fromIndex > toIndex)
3. throw new IllegalArgumentException("fromIndex(" + fromIndex +
4. ") > toIndex(" + toIndex+")");
5. if (fromIndex < 0)
6. throw new ArrayIndexOutOfBoundsException(fromIndex);
7. if (toIndex > arrayLen)
8. throw new ArrayIndexOutOfBoundsException(toIndex);
9. }

image-20221027145908490

6.5 Java技术体系平台

  • Java SE(Java Standard Edition)标准版
    • 支持面向桌面级应用(如Windows下的应用程序)的Java平台,即定位个人计算机的应用开发。
    • 包括用户界面接口AWT及Swing,网络功能与国际化、图像处理能力以及输入输出支持等。
    • 此版本以前称为J2SE
  • Java EE(Java Enterprise Edition)企业版
    • 为开发企业环境下的应用程序提供的一套解决方案,即定位在服务器端的Web应用开发
    • JavaEE是JavaSE的扩展,增加了用于服务器开发的类库。如:Servlet能够延伸服务器的功能,通过请求-响应的模式来处理客户端的请求;JSP是一种可以将Java程序代码内嵌在网页内的技术。
    • 版本以前称为J2EE
  • Java ME(Java Micro Edition)小型版
    • 支持Java程序运行在移动终端(手机、机顶盒)上的平台,即定位在消费性电子产品的应用开发
    • JavaME是JavaSE的内伸,精简了JavaSE 的核心类库,同时也提供自己的扩展类。增加了适合微小装置的类库:javax.microedition.io.*等。
    • 此版本以前称为J2ME

image-20211218093009884

注意:

Android开发不等同于Java ME的开发

7. Java开发环境搭建(掌握)

7.1 什么是JDK、JRE

  • JDK (Java Development Kit):是Java程序开发工具包,包含JRE 和开发人员使用的工具。
  • *JRE * (Java Runtime Environment) :是Java程序的运行时环境,包含JVM 和运行时所需要的核心类库

如下是Java 8.0 Platform:

image-20220310200731185

小结:

JDK = JRE + 开发工具集(例如Javac编译工具等)

JRE = JVM + Java SE标准类库

7.2 JDK版本选择

image-20221108221057540

image-20220310201541841

  • 自Java 8版本发布以来,其后的每次更新,都会有小伙伴高呼:Java8 YYDS!
  • 论坛的声音:“你发任你发,我用Java 8!

数据说话1:

JRebel 于2022年4月前后发布了《2022 年Java生态系统状况报告》,报告中提到使用Java11 的占比最多,Java 8 紧随其后,如下图。而此前2020年的报告显示,Java8占比达到了84.48%

d315

我的分析:

G1是最受欢迎的GC算法。Java 11及更高版本的G1收集器是默认的GC,而Java 8中并不是。出于对G1的喜爱,很多开发者才会选择抛弃Java 8。

数据说话2:

此外,某美国软件开发商在对近千名专业的Java开发者调研后,发布的《2022年Java开发者生产力报告》称:八年前发布的Java 8依然是Java中应用最广泛的版本,占比 37%,其次是 Java 11,占比29%

高斯林说话:


image-20220428151952049

Spring框架说话:

在Java 17正式发布之前,Java开发框架Spring率先在官博宣布,Spring Framework 6和Spring Boot 3计划在2022年第四季度实现总体可用性的高端基线:

  • Java 17+(来自 Spring Framework 5.3.x 线中的 Java 8-17)
  • Jakarta EE 9+(来自Spring框架5.3.x 线中的 Java EE 7-8)

Spring 官方说明:https://spring.io/blog/2022/01/20/spring-boot-3-0-0-m1-is-now-available

image-20220527155439683

意味着:springboot3.0 是需要用java17和spring6.0为基础建设。如果从企业选型最新springboot3.0作为架构来说,它搭配jdk17肯定是标配了。

7.3 JDK的下载

  • 下载网址(Oracle公司官网):
  • 下载步骤:如图所示,在官网底部选择Developers开发者

1572254490435

(1)在Developers页面中间的技术分类部分,选择Java,单击进入,如图所示:

image-20211019110551338

(2)这里展示的是最新Java版本,例如Java17。单击Download Java,然后选择具体的版本下载。

image-20211019110843394

(3)选择Download Java按钮后

image-20211019111110242

(4)如果想安装Java8 可以选择如下位置:

image-20211019111150970

(5)选择Accept License Agreement,

image-20211019111252989

(6)注册或登录后下载:

image-20211019111922387

(7)如果需要安装其它版本,可以选择Java archive:

image-20220801004606762

接着进行选择下载即可:

image-20220801004648997

7.4 JDK的安装

  • 安装说明
    • 傻瓜式安装,下一步即可。
    • 建议:安装路径不要有中文或者空格等特殊符号。
    • 本套课程会同时安装JDK8 和 JDK17,并以JDK17为默认版本进行讲解。
  • 安装步骤:

(1)双击jdk-17_windows-x64_bin.exe文件,并单击下一步,如图所示:

image-20220303083718546

(2)修改安装路径,单击更改,如图所示:

image-20220303083759546

(3)将安装路径修改为D:\develop_tools\jdk\jdk-17.0.2\,并单击下一步,如图所示:

image-20220303083845794

(4)稍后几秒,安装完成,如图所示:

image-20220303083905277

image-20220303083931309

7.5 配置path环境变量

7.5.1 理解path环境变量

什么是path环境变量?

答:window操作系统执行命令时,所要搜寻的路径。

为什么配置path?

答:希望在命令行使用javac.exe等工具时,任意目录下都可以找到这个工具所在的目录。

以JDK为例演示

我们在C:\Users\songhk目录下使用javac命令,结果如下:

image-20220310223852720

我们在JDK的安装目录的bin目录下使用javac命令,结果如下:

image-20220607113708022

我们不可能每次使用java.exe,javac.exe等工具的时候都进入到JDK的安装目录下,太麻烦了。这时就需要配置path环境变量。

7.5.2 JDK8配置方案1:只配置path

  • 步骤:

(1)打开桌面上的计算机,进入后在左侧找到此电脑,单击鼠标右键,选择属性,如图所示:

image-20220310224844837

(2)选择高级系统设置,如图所示:

image-20220310224912273

(3)在高级选项卡,单击环境变量,如图所示:

image-20220310224952745

(4)在系统变量中,选中Path 环境变量,双击或者点击编辑 ,如图所示:

image-20220310225023208

(5)点击新建,填入D:\develop_tools\jdk\jdk1.8.0_271\bin ,并将此值上移到变量列表的首位。如图所示:

  • 编辑模式1:
image-20220607114102135
  • 编辑模式2:(注意,结尾需要有英文模式下的;)

image-20220929104547925

(6)环境变量配置完成,重新开启DOS命令行,在任意目录下输入javacjava命令或java -version,运行成功。

image-20220607114432286 image-20220607114517652

7.5.3 JDK8配置方案2:配置JAVA_HOME+path(推荐)

  • 步骤:

(1)打开桌面上的计算机,进入后在左侧找到计算机,单击鼠标右键,选择属性,如图所示:

image-20220310224844837

(2)选择高级系统设置,如图所示:

image-20220310224912273

(3)在高级选项卡,单击环境变量,如图所示:

image-20220310224952745

(4)在系统变量中,单击新建 ,创建新的环境变量,如图所示:

image-20220310225245208

(5)变量名输入JAVA_HOME,变量值输入 D:\develop_tools\jdk\jdk1.8.0_271 ,单击确定,如图所示:

image-20220607114800372

(6)选中Path 环境变量,双击或者点击编辑 ,如图所示:

image-20220310225023208

(7)在变量值的最前面,键入%JAVA_HOME%\bin。如图所示:

image-20220607114953758

注意:强烈建议将%JAVA_HOME%\bin声明在path环境变量中所有变量的最前面!

(8)环境变量配置完成,重启DOS命令行,在任意目录下输入javacjava命令或java -version,运行成功。

image-20220607114432286 image-20220607114517652

我想说:

有的书籍、论坛、视频上还提到配置classpath,用于指名class文件识别的路径。其实是没必要的,反而建议大家如果配置了classpath环境变量,要删除。对于初学者,反而不友好。

小结如下:

image-20220607115255562

7.5.4 JDK17配置方案:自动配置

不管大家有没有提前安装JDK8或其它版本JDK,在我们安装完JDK17之后,理应按JDK8的方式配置path环境变量。但是,我们发现在安装完JDK17以后,配置环境变量之前,执行CMD指令:

image-20220607115700870

竟然成功了!而且是17.0.2版本。因为JDK17在安装之后,自动进行了环境变量的配置。如下:

image-20220607115840388

这里建议,将JDK17安装的路径,设置为JAVA_HOME,并将%JAVA_HOME%\bin上移到首位。

image-20221110142710972 image-20221110142734279

思考:如果你仍然希望在JDK8下开发Java程序?如何做呢?

8. 开发体验:HelloWorld(掌握)

JDK安装完毕,我们就可以开发第一个Java程序了,习惯性的称为:HelloWorld。

8.1 开发步骤

Java程序开发三步骤:编写编译运行

  • 将 Java 代码编写到扩展名为 .java 的源文件中
  • 通过 javac.exe 命令对该 java 文件进行编译,生成一个或多个字节码文件
  • 通过 java.exe 命令对生成的 class 文件进行运行

image-20220310230210728

8.2 编写

(1)在D:\JavaSE\chapter01 目录下新建文本文件,完整的文件名修改为HelloWorld.java,其中文件名为HelloWorld,后缀名必须为.java

image-20220801010222222

(2)用记事本或editplus等文本编辑器打开(虽然记事本也可以,但是没有关键字颜色标识,不利于初学者学习)

(3)在文件中输入如下代码,并且保存:

1
2
3
4
5
class HelloChina {
public static void main(String[] args) {
System.out.println("HelloWorld!!");
}
}
  • 友情提示1:每个字母和符号必须与示例代码一模一样,包括大小写在内。

  • 友情提示2:

image-20220310230618659

image-20220521173215013

第一个HelloWord 源程序就编写完成了,但是这个文件是程序员编写的,JVM是看不懂的,也就不能运行,因此我们必须将编写好的Java源文件 编译成JVM可以看懂的字节码文件 ,也就是.class文件。

8.3 编译

在DOS命令行中,进入D:\JavaSE\chapter01目录,使用javac 命令进行编译。

使用文件资源管理器打开D:\JavaSE\chapter01目录,然后在地址栏输入cmd。

image-20220801010435144

命令:

1
javac Java源文件名.后缀名java

举例:

1
javac HelloWorld.java
image-20220801010840640

编译成功后,命令行没有任何提示。打开D:\JavaSE\chapter01目录,发现产生了一个新的文件 HelloChina.class,该文件就是编译后的文件,是Java的可运行文件,称为字节码文件,有了字节码文件,就可以运行程序了。

8.4 运行

在DOS命令行中,在字节码文件目录下,使用java 命令进行运行。

命令:

1
java 主类名字

主类是指包含main方法的类,main方法是Java程序的入口:

1
2
3
public static void main(String[] args){

}

举例:

1
java HelloChina

错误演示:

java HelloChina.class

image-20220801011104381

9. Java开发工具

9.1 都有哪些开发Java的工具

  • 级别一:文本开发工具
image-20220513185907022
  • 级别二:集成开发环境(Integrated Development Environment,IDE)

把代码编写,编译,执行,调试等多种功能综合到一起的开发工具。

image-20220513190655773

9.2 如何选择

前期我们先使用文本开发工具,培养代码感,利于公司笔、面试。

后期我们使用IDE,提供更强大的功能支持

10. HelloWorld案例常见错误

10.1 拼写问题

  • 单词拼写问题
    • 正确:class 错误:Class
    • 正确:String 错误:string
    • 正确:System 错误:system
    • 正确:main 错误:mian
  • Java语言是一门严格区分大小写的语言
  • 标点符号使用问题
    • 不能用中文符号,英文半角的标点符号(正确)
    • 括号问题,成对出现

10.2 编译、运行路径问题

举例1:

image-20220310231657692
  • 源文件名不存在或者写错
  • 当前路径错误
  • 后缀名隐藏问题

举例2:

image-20220310231717893
  • 类文件名写错,尤其文件名与类名不一致时,要小心
  • 类文件不在当前路径下,或者不在classpath指定路径下

10.3 语法问题

举例1:

image-20220310231746581

声明为public的类应与文件名一致,否知编译失败。

举例2:

image-20220310231815405

编译失败,注意错误出现的行数,再到源代码中指定位置改错

10.4 字符编码问题

当cmd命令行窗口的字符编码与.java源文件的字符编码不一致,如何解决?

1557881223916

解决方案一:

  • 在Notepad++等编辑器中,修改源文件的字符编码:
1557881271819
  • 在EditPlus中可以将Java源文件另存为ANSI编码方式(中文操作系统下即为GBK字符集)
1658810752424

解决方案二:

在使用javac命令式,可以指定源文件的字符编码
1
javac -encoding utf-8 Review01.java

10.5 建议

  • 注意缩进!

    • 一定要有缩进。缩进就像人得体的衣着一样!

    • 只要遇到{}就缩进,缩进的快捷键tab键。

  • 必要的空格

    • 变量类型、变量、赋值符号、变量值之间填充相应空格,更美观。比如: int num = 10;

11. HelloWorld小结

11.1 Java程序的结构与格式

结构:

1
2
3
4
5
类{
方法{
语句;
}
}

格式:

(1)每一级缩进一个Tab键

(2){}的左半部分在行尾,右半部分单独一行,与和它成对的”{“的行首对齐

11.2 Java程序的入口

Java程序的入口是main方法

1
2
3
public static void main(String[] args){

}

11.3 两种常见的输出语句

  • 换行输出语句:输出内容,完毕后进行换行,格式如下:

    1
    System.out.println(输出内容);
  • 直接输出语句:输出内容,完毕后不做任何处理,格式如下

    1
    System.out.print(输出内容);

注意事项:

​ 换行输出语句,括号内可以什么都不写,只做换行处理

​ 直接输出语句,括号内什么都不写的话,编译报错

11.4 源文件名与类名

(1)源文件名是否必须与类名一致?public呢?

1
2
3
4
5
如果这个类不是public,那么源文件名可以和类名不一致。但是不便于代码维护。

如果这个类是public,那么要求源文件名必须与类名一致。否则编译报错。

我们建议大家,不管是否是public,都与源文件名保持一致,而且一个源文件尽量只写一个类,目的是为了好维护。

(2)一个源文件中是否可以有多个类?public呢?

1
2
3
一个源文件中可以有多个类,编译后会生成多个.class字节码文件。

但是一个源文件只能有一个public的类。

12. 注释(comment)

  • 什么是注释?

    • 源文件中用于解释、说明程序的文字就是注释。
  • 注释是一个程序员必须要具有的良好编程习惯。实际开发中,程序员可以先将自己的思想通过注释整理出来,再用代码去体现。

    程序员最讨厌两件事:

    一件是自己写代码被要求加注释

    另一件是接手别人代码,发现没有注释

  • 不加注释的危害

  • Java中的注释类型:

    • 单行注释

      1
      //注释文字
    • 多行注释

      1
      2
      3
      4
      5
      /* 
      注释文字1
      注释文字2
      注释文字3
      */
    • 文档注释 (Java特有)

      1
      2
      3
      4
      /**
      @author 指定java程序的作者
      @version 指定源文件的版本
      */
  • 注释的作用

    • 它提升了程序的可阅读性。(不加注释的危害性,见图。)
    • 调试程序的重要方法。
  • 具体使用1:单行注释、多行注释

    • 对于单行和多行注释,被注释的文字,不会出现在字节码文件中,进而不会被JVM(java虚拟机)解释执行。

    • 多行注释里面不允许有多行注释嵌套。

    • 一个段子

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      A:嘿 //是什么意思啊?
      B:嘿.
      A:呃 我问你//是什么意思?
      B:问吧.
      A:我刚才不是问了么?
      B:啊?
      A:你再看看记录...
      B:看完了.
      A:......所以//是啥?
      B:所以什么?
      A:你存心耍我呢吧?
      B:没有啊 你想问什么?
      ……
      不断循环之后,A一气之下和B绝交,自己苦学程序。
      N年之后,A终于修成正果,回想起B,又把聊天记录翻出来看,这时,他突然发现B没有耍他……
      而他自己也不知道当年他问B的究竟是什么问题……
  • 具体使用2:文档注释(Java特有)

    • 文档注释内容可以被JDK提供的工具 javadoc 所解析,生成一套以网页文件形式体现的该程序的说明文档。

    • 操作方式。比如:

      1
      javadoc -d mydoc -author -version HelloWorld.java
image-20220310232527578

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//单行注释
/*
多行注释
*/
/**
文档注释演示。这是我的第一个Java程序!^_^
@author songhk
@version 1.0
*/
public class HelloWorld{

/**
Java程序的入口
@param args main方法的命令参数
*/
public static void main(String[] args){
System.out.println("hello");
}
}

13. Java API文档

  • API (Application Programming Interface,应用程序编程接口)是 Java 提供的基本编程接口。
  • Java语言提供了大量的基础类,因此 Oracle 也为这些基础类提供了相应的说明文档,用于告诉开发者如何使用这些类,以及这些类里包含的方法。大多数Java书籍中的类的介绍都要参照它来完成,它是编程者经常查阅的资料。
  • Java API文档,即为JDK使用说明书、帮助文档。类似于:
新华字典

14. Java核心机制:JVM

14.1 Java语言的优缺点

Java确实是从C语言和C++语言继承了许多成份,甚至可以将Java看成是类C语言发展和衍生的产物。“青出于蓝,而胜于蓝”。

14.1.1 优点

  • 跨平台性:这是Java的核心优势。Java在最初设计时就很注重移植和跨平台性。比如:Java的int永远都是32位。不像C++可能是16,32,可能是根据编译器厂商规定的变化。
  • 通过Java语言编写的应用程序在不同的系统平台上都可以运行。“Write once , Run Anywhere”。
  • 原理:只要在需要运行 java 应用程序的操作系统上,先安装一个Java虚拟机 (JVM ,Java Virtual Machine) 即可。由JVM来负责Java程序在该系统中的运行。
JVM的跨平台性

image-20211217111217831

  • 面向对象性:

    面向对象是一种程序设计技术,非常适合大型软件的设计和开发。面向对象编程支持封装、继承、多态等特性,让程序更好达到高内聚低耦合的标准。

  • 健壮性:吸收了C/C++语言的优点,但去掉了其影响程序健壮性的部分(如指针、内存的申请与释放等),提供了一个相对安全的内存管理和访问机制。

  • 安全性高:

    Java适合于网络/分布式环境,需要提供一个安全机制以防恶意代码的攻击。如:安全防范机制(ClassLoader类加载器),可以分配不同的命名空间以防替代本地的同名类、字节代码检查。

  • 简单性:

    Java就是C++语法的简化版,我们也可以将Java称之为“C++--”。比如:头文件,指针运算,结构,联合,操作符重载,虚基类等。

  • 高性能:

    • Java最初发展阶段,总是被人诟病“性能低”;客观上,高级语言运行效率总是低于低级语言的,这个无法避免。Java语言本身发展中通过虚拟机的优化提升了几十倍运行效率。比如,通过JIT(JUST IN TIME)即时编译技术提高运行效率。

    • Java低性能的短腿,已经被完全解决了。业界发展上,我们也看到很多C++应用转到Java开发,很多C++程序员转型为Java程序员。

14.1.2 缺点

  • 语法过于复杂、严谨,对程序员的约束比较多,与python、php等相比入门较难。但是一旦学会了,就业岗位需求量大,而且薪资待遇节节攀升
  • 一般适用于大型网站开发,整个架构会比较重,对于初创公司开发和维护人员的成本比较高(即薪资高),选择用Java语言开发网站或应用系统的需要一定的经济实力。
  • 并非适用于所有领域。比如,Objective C、Swift在iOS设备上就有着无可取代的地位。浏览器中的处理几乎完全由JavaScript掌控。Windows程序通常都用C++或C#编写。Java在服务器端编程和跨平台客户端应用领域则很有优势。

14.2 JVM功能说明

JVMJava Virtual Machine ,Java虚拟机):是一个虚拟的计算机,是Java程序的运行环境。JVM具有指令集并使用不同的存储区域,负责执行指令,管理数据、内存、寄存器。

image-20220310165805739

14.2.1 功能1:实现Java程序的跨平台性

我们编写的Java代码,都运行在JVM 之上。正是因为有了JVM,才使得Java程序具备了跨平台性。

image-20220513191856727

使用JVM前后对比:

image-20220514151716805

14.2.2 功能2:自动内存管理(内存分配、内存回收)

  • Java程序在运行过程中,涉及到运算的数据的分配存储等都由JVM来完成
  • Java消除了程序员回收无用内存空间的职责。提供了一种系统级线程跟踪存储空间的分配情况,在内存空间达到相应阈值时,检查并释放可被释放的存储器空间。
  • GC的自动回收,提高了内存空间的利用效率,也提高了编程人员的效率,很大程度上减少了因为没有释放空间而导致的内存泄漏

面试题:

Java程序还会出现内存溢出和内存泄漏问题吗? Yes!

15. 章节案例

案例1:个人信息输出

image-20220315221336038

1
2
3
4
5
6
7
8
class Exercise1{
public static void main(String[] args){
System.out.println("姓名:康师傅");
System.out.println();//换行操作
System.out.println("性别:男");
System.out.println("家庭住址:北京程序员聚集地:回龙观");
}
}

案例2:输出:心形

结合\n(换行),\t(制表符),空格等在控制台打印出如下图所示的效果。

image-20220309004152219

方式一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
//方式一:
class Exercise2{
public static void main(String[] args){
System.out.print("\t");
System.out.print("*");
System.out.print("\t");
System.out.print("\t");
System.out.print("\t");
System.out.print("\t");
System.out.print("\t");
System.out.print("\t");
System.out.print("\t");

System.out.println("*");


System.out.print("*");
System.out.print("\t");
//System.out.print("\t");
System.out.print("\t");
System.out.print("\t");
System.out.print("I love java");
System.out.print("\t");
System.out.print("\t");
System.out.print("\t");
System.out.print("\t");
System.out.print("\t");
System.out.println("*");


System.out.print("\t");
System.out.print("*");
System.out.print("\t");
System.out.print("\t");
System.out.print("\t");
System.out.print("\t");
System.out.print("\t");
System.out.print("\t");
System.out.print("\t");

System.out.println("*");


System.out.print("\t");
System.out.print("\t");
System.out.print("*");
System.out.print("\t");
System.out.print("\t");
System.out.print("\t");
System.out.print("\t");
System.out.print("\t");

System.out.println("*");


System.out.print("\t");
System.out.print("\t");
System.out.print("\t");
System.out.print("*");
System.out.print("\t");
System.out.print("\t");
System.out.print("\t");

System.out.println("*");


System.out.print("\t");
System.out.print("\t");
System.out.print("\t");
System.out.print("\t");
System.out.print("*");
System.out.print("\t");

System.out.println("*");


System.out.print("\t");
System.out.print("\t");
System.out.print("\t");
System.out.print("\t");
System.out.print(" ");
System.out.print("*");

}

}

方式二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Exercise3{
public static void main(String[] args){

System.out.print("\t"+"*"+"\t\t\t\t\t\t\t\t\t\t\t\t"+"*"+"\t"+"\n");
System.out.print("*"+"\t\t"+"*"+"\t\t\t\t"+"I love Java"+"\t\t\t\t"+"*"+"\t\t\t"+"*"+"\n");
System.out.print("\t"+"*"+"\t\t\t\t\t\t\t\t\t\t\t\t"+"*"+"\t"+"\n");
System.out.print("\t\t"+"*"+"\t\t\t\t\t\t\t\t\t\t"+"*"+"\t\t"+"\n");
System.out.print("\t\t\t"+"*"+"\t\t\t\t\t\t\t\t"+"*"+"\t"+"\n");
System.out.print("\t\t\t\t"+"*"+"\t\t\t\t\t\t"+"*"+""+"\t"+"\n");
System.out.print("\t\t\t\t\t"+"*"+"\t\t\t\t"+"*"+""+"\t\t"+"\n");
System.out.print("\t\t\t\t\t\t"+"*"+"\t\t"+"*"+""+"\t\t"+"\n");
System.out.print("\t\t\t\t\t\t\t"+"*"+"\n");


}

}

第02章_变量与运算符


本章专题与脉络

第1阶段:Java基本语法-第02章

1. 关键字(keyword)

  • 定义:被Java语言赋予了特殊含义,用做专门用途的字符串(或单词)
    • HelloWorld案例中,出现的关键字有 classpublicstaticvoid 等,这些单词已经被Java定义好了。
  • 特点:全部关键字都是小写字母
  • 关键字比较多,不需要死记硬背,学到哪里记到哪里即可。
  • 官方地址: https://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.html

image-20220310234414849

说明:

  1. 关键字一共50个,其中constgoto保留字(reserved word)。
  2. truefalsenull不在其中,它们看起来像关键字,其实是字面量,表示特殊的布尔值和空值。
image-20220310234557454 image-20220801143635356

2. 标识符( identifier)

Java中变量、方法、类等要素命名时使用的字符序列,称为标识符。

技巧:凡是自己可以起名字的地方都叫标识符。(自己定义类名/自己定义变量名/自己定义方法名)

标识符的命名规则(必须遵守的硬性规定):

1
2
3
4
5
> 由26个英文字母大小写,0-9 ,_或 $ 组成  
> 数字不可以开头。
> 不可以使用关键字和保留字,但能包含关键字和保留字。
> Java中严格区分大小写,长度无限制。
> 标识符不能包含空格。

练习:miles、Test、a++、 –a、4#R、$4、 #44、apps、class、public、int、x、y、radius

标识符的命名规范(建议遵守的软性要求,否则工作时容易被鄙视):

1
2
3
4
5
6
7
8
9
10
11
> 包名:多单词组成时所有字母都小写:xxxyyyzzz。
例如:java.lang、com.atguigu.bean

> 类名、接口名:多单词组成时,所有单词的首字母大写:XxxYyyZzz
例如:HelloWorld,String,System等

> 变量名、方法名:多单词组成时,第一个单词首字母小写,第二个单词开始每个单词首字母大写:xxxYyyZzz
例如:age,name,bookName,main,binarySearch,getName

> 常量名:所有字母都大写。多单词时每个单词用下划线连接:XXX_YYY_ZZZ
例如:MAX_VALUE,PI,DEFAULT_CAPACITY

注意:在起名字时,为了提高阅读性,要尽量有意义,“见名知意”。

更多细节详见《代码整洁之道_关于标识符.txt》《阿里巴巴Java开发手册-1.7.1-黄山版》

3. 变量

3.1 为什么需要变量

image-20220513235020527

一花一世界,如果把一个程序看做一个世界或一个社会的话,那么变量就是程序世界的花花草草、万事万物。即,变量是程序中不可或缺的组成单位,最基本的存储单元

image-20220513235828042

3.2 初识变量

  • 变量的概念:

    • 内存中的一个存储区域,该区域的数据可以在同一类型范围内不断变化

    • 变量的构成包含三个要素:数据类型变量名存储的值

    • Java中变量声明的格式:数据类型 变量名 = 变量值

      image-20220311000744867
  • 变量的作用:用于在内存中保存数据。

  • 使用变量注意:

    • Java中每个变量必须先声明,后使用。 ①先声明后赋值 int age; age=10; ②声明同时赋值 int age=25;
    • 使用变量名来访问这块区域的数据。
    • 变量的作用域:其定义所在的一对{ }内。
    • 变量只有在其作用域内才有效。出了作用域,变量不可以再被调用。
    • 同一个作用域内,不能定义重名的变量。(不同域可以同名)

3.3 Java中变量的数据类型

Java中变量的数据类型分为两大类:

  • 基本数据类型:包括 整数类型浮点数类型字符类型布尔类型

  • 引用数据类型:包括数组接口枚举注解记录

    Java的数据类型

3.4 变量的使用

3.4.1 步骤1:变量的声明

1
格式:数据类型  变量名;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//例如:
//存储一个整数类型的年龄
int age;

//存储一个小数类型的体重
double weight;

//存储一个单字符类型的性别
char gender;

//存储一个布尔类型的婚姻状态
boolean marry;

//存储一个字符串类型的姓名
String name;

//声明多个同类型的变量
int a,b,c; //表示a,b,c三个变量都是int类型。

注意:变量的数据类型可以是基本数据类型,也可以是引用数据类型。

3.4.2 步骤2:变量的赋值

给变量赋值,就是把“值”存到该变量代表的内存空间中。同时,给变量赋的值类型必须与变量声明的类型一致或兼容。

变量赋值的语法格式:

1
变量名 = 值;

举例1:可以使用合适类型的常量值给已经声明的变量赋值

1
2
3
age = 18;
weight = 109;
gender = '女';

举例2:可以使用其他变量或者表达式给变量赋值

1
2
3
4
5
6
int m = 1;
int n = m;

int x = 1;
int y = 2;
int z = 2 * x + y;

3:变量可以反复赋值

1
2
3
4
5
6
7
//先声明,后初始化
char gender;
gender = '女';

//给变量重新赋值,修改gender变量的值
gender = '男';
System.out.println("gender = " + gender);//gender = 男

举例4:也可以将变量的声明和赋值一并执行

1
2
boolean isBeauty = true;
String name = "迪丽热巴";

内存结构如图:

image-20220514152216148

4. 基本数据类型介绍

4.1 整数类型:byte、short、int(默认)、long

image-20220520111756274
  • Java各整数类型有固定的表数范围和字段长度,不受具体操作系统的影响,以保证Java程序的可移植性。
image-20220311001553945
  • 定义long类型的变量,赋值时需要以”l“或”L“作为后缀。 – 例如:long age=12l
  • Java程序中变量通常声明为int型,除非不足以表示较大的数,才使用long。
  • Java的整型常量默认为 int 型

4.1.1 补充:计算机存储单位

  • 字节(Byte):是计算机用于计量存储容量基本单位,一个字节等于8 bit。

  • 位(bit):是数据存储的最小单位。二进制数系统中,每个0或1就是一个位,叫做bit(比特),其中8 bit 就称为一个字节(Byte)。

  • 转换关系:

    • 8 bit = 1 Byte
    • 1024 Byte = 1 KB
    • 1024 KB = 1 MB
    • 1024 MB = 1 GB
    • 1024 GB = 1 TB

4.2 浮点类型:float、double(默认)

  • 与整数类型类似,Java 浮点类型也有固定的表数范围和字段长度,不受具体操作系统的影响。
image-20220311001749699
  • 浮点型常量有两种表示形式:
    • 十进制数形式。如:5.12 512.0f .512 (必须有小数点)
    • 科学计数法形式。如:5.12e2 512E2 100E-2
  • float:单精度,尾数可以精确到7位有效数字。很多情况下,精度很难满足需求。
  • double(通常采用此类型):双精度,精度是float的两倍。
  • 定义float类型的变量,赋值时需要以”f“或”F“作为后缀。 – 例如:float age=12f
  • Java 的浮点型常量默认为double型

4.2.1 关于浮点型精度的说明

  • 并不是所有的小数都能可以精确的用二进制浮点数表示。二进制浮点数不能精确的表示0.1、0.01、0.001这样10的负次幂。

  • 浮点类型float、double的数据不适合在不容许舍入误差的金融计算领域。如果需要精确数字计算或保留指定位数的精度,需要使用BigDecimal类。(算法题经常出现)

  • 测试用例:

1
2
3
4
5
6
7
8
9
//测试1:(解释见章末企业真题:为什么0.1 + 0.2不等于0.3)
System.out.println(0.1 + 0.2);//0.30000000000000004

//测试2:
float ff1 = 123123123f;
float ff2 = ff1 + 1;
System.out.println(ff1);
System.out.println(ff2);
System.out.println(ff1 == ff2); #输出是True 因为精度太差所以竟然是相等的!!!!!

4.2.2 应用举例

案例1:定义圆周率并赋值为3.14,现有3个圆的半径分别为1.2、2.5、6,求它们的面积。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @author 尚硅谷-宋红康
* @create 12:36
*/
public class Exercise1 {
public static void main(String[] args) {
double PI = 3.14; //圆周率

double radius1 = 1.2;
double radius2 = 2.5;
int radius3 = 6;

System.out.println("第1个圆的面积:" + PI * radius1 * radius1);
System.out.println("第2个圆的面积:" + PI * radius2 * radius2);
System.out.println("第3个圆的面积:" + PI * radius3 * radius3);
}
}

案例2:小明要到美国旅游,可是那里的温度是以华氏度为单位记录的。
它需要一个程序将华氏温度(80度)转换为摄氏度,并以华氏度和摄氏度为单位分别显示该温度。

1
℃ = (℉ - 32) / 1.8
1
2
3
4
5
6
7
8
9
10
11
/**
* @author 尚硅谷-宋红康
* @create 12:51
*/
public class Exercise2 {
public static void main(String[] args) {
double hua = 80;
double she = (hua-32)/1.8;
System.out.println("华氏度" + hua+"℉转为摄氏度是" +she+"℃");
}
}

4.3 字符类型:char

  • char 型数据用来表示通常意义上“字符”(占2字节)

  • Java中的所有字符都使用Unicode编码,故一个字符可以存储一个字母,一个汉字,或其他书面语的一个字符。

  • 字符型变量的三种表现形式:

    • 形式1:使用单引号(‘ ‘)括起来的单个字符

      例如:char c1 = ‘a’; char c2 = ‘中’; char c3 = ‘9’;

    • 形式2:直接使用 Unicode值来表示字符型常量:‘\uXXXX’。其中,XXXX代表一个十六进制整数。

      例如:\u0023 表示 ‘#’。

    • 形式3:Java中还允许使用转义字符‘\’来将其后的字符转变为特殊字符型常量。

      例如:char c3 = ‘\n’; // ‘\n’表示换行符

    转义字符 说明 Unicode表示方式
    \n 换行符 \u000a
    \t 制表符 \u0009
    \" 双引号 \u0022
    \' 单引号 \u0027
    \\ 反斜线 \u005c
    \b 退格符 \u0008
    \r 回车符 \u000d
  • char类型是可以进行运算的。因为它都对应有Unicode码,可以看做是一个数值。

  • 举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class IdentifierTest {
public static void main(String[] args){
//形式1 使用单引号(' ')括起来的单个字符
char bl='a';
System.out.println(bl); // a
char zg='中';
System.out.println(zg); // 中

//形式2 直接使用 `Unicode值`来表示字符型常量:
char qiguai='\u0036';
System.out.println(qiguai); // 6


//形式3 使用`转义字符‘\’` 来将其后的字符转变为特殊字符型常量。
char c9='\n';
char c10='\t';
// hello
// world
System.out.println("hello"+c9+"world");
// hello world
System.out.println("hello"+c10+"world");
}
}

4.4 布尔类型:boolean

  • boolean 类型用来判断逻辑条件,一般用于流程控制语句中:

    • if条件控制语句;
    • while循环控制语句;
    • for循环控制语句;
    • do-while循环控制语句;
  • boolean类型数据只有两个值:true、false,无其它。

    • 不可以使用0或非 0 的整数替代false和true,这点和C语言不同。
    • 拓展:Java虚拟机中没有任何供boolean值专用的字节码指令,Java语言表达所操作的boolean值,在编译之后都使用java虚拟机中的int数据类型来代替:true用1表示,false用0表示。——《java虚拟机规范 8版》
  • 举例:

1
2
3
4
5
6
7
boolean isFlag = true;

if(isFlag){
//true分支
}else{
//false分支
}

经验之谈:

Less is More!建议不要这样写:if ( isFlag = = true ),只有新手才如此。关键也很容易写错成if(isFlag = true),这样就变成赋值isFlag为true而不是判断!老鸟的写法是if (isFlag)或者if ( !isFlag)。

5. 基本数据类型变量间运算规则

在Java程序中,不同的基本数据类型(只有7种,不包含boolean类型)变量的值经常需要进行相互转换。

转换的方式有两种:①自动类型提升和②强制类型转换

5.1 自动类型提升

规则:将取值范围小(或容量小)的类型自动提升为取值范围大(或容量大)的类型 。

image-20220523162200891

基本数据类型的转换规则如图所示:

image-20220311002543688

(1)当把存储范围小的值(常量值、变量的值、表达式计算的结果值)赋值给了存储范围大的变量时

1
2
3
4
5
6
int i = 'A';//char自动升级为int,其实就是把字符的编码值赋值给i变量了
double d = 10;//int自动升级为double
long num = 1234567; //右边的整数常量值如果在int范围呢,编译和运行都可以通过,这里涉及到数据类型转换

//byte bigB = 130;//错误,右边的整数常量值超过byte范围
long bigNum = 12345678912L;//右边的整数常量值如果超过int范围,必须加L,显式表示long类型。否则编译不通过

(2)当存储范围小的数据类型与存储范围大的数据类型变量一起混合运算时,会按照其中最大的类型运算(自动转换为最大类型)。

1
2
3
4
5
int i = 1;
byte b = 1;
double d = 1.0;

double sum = i + b + d;//混合运算,升级为double 输出为3.0

(3)当byte,short,char(三个在int之前的不分前后的类型)数据类型的变量进行算术运算时,按照int类型处理。

1
2
3
4
5
6
7
8
byte b1 = 1;
byte b2 = 2;
byte b3 = b1 + b2;// byte编译报错,b1 + b2自动升级为int,所以是需要int来接结果!!!!!

char c1 = '0'; // int值为48
char c2 = 'A'; // int值为65
int i = c1 + c2; //至少需要使用int类型来接收
System.out.println(c1 + c2); //48+65=113

练习:

1
2
3
4
5
6
7
8
9
10
11
12
设 x 为float型变量,y为double型变量,a为int型变量,b为long型变量,c为char型变量,则表达式
x + y * a / x + b / y + c的值类型为:
代码如下:
float x=1.0f;
double y=2.0;
int a=1;
long b=3l;
char c='a'; // int值为97
double sum = x + y * a / x + b / y + c;
System.out.println(x+y*a/x+b/y+c); //101.5

A. int B. long C. double D. char

5.2 强制类型转换

3.14 赋值到int 类型变量会发生什么?产生编译失败,肯定无法赋值。

1
int i = 3.14; // 编译报错

想要赋值成功,只有通过强制类型转换,将double 类型强制转换成int 类型才能赋值。

image-20220523162407722

规则:将取值范围大(或容量大)的类型强制转换成取值范围小(或容量小)的类型。

自动类型提升是Java自动执行的,而强制类型转换是自动类型提升的逆运算,需要我们自己手动执行。

转换格式:

1
数据类型1 变量名 = (数据类型1)被强转数据值;  //()中的数据类型必须<=变量值的数据类型

(1)当把存储范围大的值(常量值、变量的值、表达式计算的结果值)强制转换为存储范围小的变量时,可能会损失精度溢出

1
2
3
4
5
6
7
int i = (int)3.14;//损失精度  输出为3 截断了后面的0.14

double d = 1.2;
int num = (int)d;//损失精度 输出为1 截断了后面的0.2

int i = 200;
byte b = (byte)i;//溢出 输出负56 因为原码补码问题导致

(2)当某个值想要提升数据类型时(本身可以通过自动类型提升完成),也可以使用强制类型转换。这种情况的强制类型转换是没有风险的,通常省略。

1
2
3
4
int i = 1;
int j = 2;
double bigger = (double)(i/j); // 0.0
int bigger2=i/j; // 0 可能想要一个double类型 所以使用强制类型转换才可以做到成为0.0

(3)声明变量时,long(可以√省略),float(不可以×省略)。

1
2
3
4
5
6
7
8
9
10
11
12
13
//记忆方法:  可以看作long定义的时候可能是int(整数型默认)自动类型转换为long   所以可以省略
// 可以看做float定义的时候可能是double(浮点数默认)没办法自动类型转换为double 所有不可以省略

long l1 = 123L;
long l2 = 123;//如何理解呢? 此时可以看做是int类型的123自动类型提升为long类型

//long l3 = 123123123123; //报错,因为123123123123超出了int的范围。
long l4 = 123123123123L;


//float f1 = 12.3; //报错,因为12.3看做是double,不能自动转换为float类型
float f2 = 12.3F;
float f3 = (float)12.3;

练习:判断是否能通过编译

1
2
3
4
5
6
7
8
9
10
11
12
13
1short  s = 5;
s = s-2; //判断:no 因为short-int的结果是一个int(往上自动类型转换) 所以需要强制类型转换
2byte b = 3;
b = b + 4; //判断:no 因为byte+int的结果是一个int(往上自动类型转换) 所以需要强制类型转换
b = (byte)(b+4); //判断:yes 要强制类型转换
3char c = ‘a’;
int i = 5;
float d = .314F;
double result = c+i+d; //判断:yes 因为是char+int+float 结果是一个float 可以自动类型转换为double
4byte b = 5;
short s = 3;
short t = s + b; //判断:no 因为当byte,short数据类型进行算术运算时,按照int类型
//可以改成short t = (short) (s + b);

*问答:为什么标识符的声明规则里要求不能数字开头? * — 是本身的数值 / 他是一个变量所对应的值

1
2
3
4
//如果允许数字开头,则如下的声明编译就可以通过:
int 123L = 12;
//进而,如下的声明中l的值到底是123?还是变量123L对应的取值12呢? 出现歧义了。
long l = 123L; // 123还是12

5.3 基本数据类型与String的运算

5.3.1 字符串类型:String

  • String不是基本数据类型,属于引用数据类型 【所以不能够进行基本数据类型的自动类型转换和强制类型转换】
  • 使用一对""来表示一个字符串,内部可以包含0-n个字符
  • 声明方式与基本数据类型类似。例如:String str = “尚硅谷”;

5.3.2 运算规则

1、任意八种基本数据类型的数据与String类型只能进行连接“+”运算,且结果一定也是String类型

1
2
3
4
5
6
7
8
9
10
11
System.out.println("   " + 1 + 2); //空格空格空格12  

int num = 10;
boolean b1 = true;
String s1 = "abc";

String s2 = s1 + num + b1;
System.out.println(s2); //abc10true

//String s3 = num + b1 + s1; //编译不通过,因为int类型不能与boolean运算
String s4 = num + (b1 + s1); //编译通过 10trueabc [b1+s1先成为字符串tureabc,然后int+string成为string]

2、String类型不能通过强制类型()转换,转为其他的类型

1
2
3
4
String str = "123";
int num = (int)str;//错误的

int num = Integer.parseInt(str);//借助包装类的方法才能转

5.3.3 案例与练习

案例:公安局身份登记

要求填写自己的姓名、年龄、性别、体重、婚姻状况(已婚用true表示,单身用false表示)、联系方式等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* @author 尚硅谷-宋红康
* @create 12:34
*/
public class Info {
public static void main(String[] args) {
String name = "康师傅";
int age = 37;
char gender = '男';
double weight = 145.6;
boolean isMarried = true;
String phoneNumber = "13112341234";

System.out.println("姓名:" + name);
System.out.println("年龄:" + age);
System.out.println("性别:" + gender);
System.out.println("体重:" + weight);
System.out.println("婚否:" + isMarried);
System.out.println("电话:" + phoneNumber);
//或者
System.out.println("name = " + name + ",age = " + age + ",gender = " +
gender + ",weight = " + weight + ",isMarried = " + isMarried +
",phoneNumber = " + phoneNumber);
}
}

练习:

练习1:

1
2
3
4
5
6
7
String str1 = 4;                       //判断对错: × 可以改为String str1=String.valueOf(4);
String str2 = 3.5f + ""; //判断str2对错:对 float+string=string类型
System.out.println(str2); //输出:3.5
System.out .println(3+4+"Hello!"); //输出:7Hello! 注意从左往右先算最后和string变为字符串
System.out.println("Hello!"+3+4); //输出:Hello!34
System.out.println('a'+1+"Hello!"); //输出:98Hello! 注意从左往右先算最后和string变为字符串
System.out.println("Hello"+'a'+1); //输出:Helloa1

练习2:

1
2
3
4
5
6
7
8
System.out.println("*    *");				//输出: *    *
System.out.println("*\t*"); //输出: * *
System.out.println("*" + "\t" + "*"); //输出: * *
System.out.println('*' + "\t" + "*"); //输出: * *
System.out.println('*' + '\t' + "*"); //输出: 51* '*'是char类型在进行算数运算时转为int类型计算!!!
System.out.println('*' + "\t" + '*'); //输出: * *
System.out.println("*" + '\t' + '*'); //输出: * *
System.out.println('*' + '\t' + '*'); //输出: 93 '*'是char类型在进行算数运算时转为int类型计算!!!

6. 计算机底层如何存储数据

计算机世界中只有二进制,所以计算机中存储和运算的所有数据都要转为二进制。包括数字、字符、图片、声音、视频等。

010101

世界上有10种人 ,认识和不认识二进制的。

6.1 进制的分类

  • 十进制(decimal)

    • 数字组成:0-9
    • 进位规则:满十进一
  • 二进制(binary)

    • 数字组成:0-1
    • 进位规则:满二进一,以0b0B开头
  • 八进制(octal):很少使用

    • 数字组成:0-7
    • 进位规则:满八进一,以数字0开头表示
  • 十六进制

    • 数字组成:0-9,a-f
    • 进位规则:满十六进一,以0x0X开头表示。此处的 a-f 不区分大小写

代码演示:

1
2
3
4
5
6
7
8
9
class BinaryTest {
public static void main(String[] args) {
int n1=123; //十进制 1*10^2+2*10^1+3*10^0=100+20+3=
int n2=0b11; //二进制 1*2^1+1*2^0=2+1=3
int n3=071; //八进制 7*8^1+1*8^0=56+1=57
int n4=0x12f;//十六进制 1*16^2+2*16^1+15*16^0=256+32+15=303
System.out.println("n1:"+n1+"\nn2:"+n2+"\nn3:"+n3+"\nn4:"+n4);
}
}

6.2 进制的换算举例

十进制 二进制 八进制 十六进制
0 0 0 0
1 1 1 1
2 10 2 2
3 11 3 3
4 100 4 4
5 101 5 5
6 110 6 6
7 111 7 7
8 1000 10 8
9 1001 11 9
10 1010 12 a或A
11 1011 13 b或B
12 1100 14 c或C
13 1101 15 d或D
14 1110 16 e或E
15 1111 17 f或F
16 10000 20 10

6.3 二进制的由来

二进制,是计算技术中广泛采用的一种数制,由德国数理哲学大师莱布尼茨于1679年发明。

二进制数据是用0和1两个数码来表示的数。它的基数为2,进位规则是“逢二进一”。

二进制广泛应用于我们生活的方方面面。比如,广泛使用的摩尔斯电码(Morse Code),它由两种基本信号组成:短促的点信号“·”,读“”;保持一定时间的长信号“”,读“”。然后,组成了26个字母,从而拼写出相应的单词。

image-20220520105721126

记忆技巧:

morsecode

我们偶尔会看到的:SOS,即为:

image-20220520110206899

6.4 二进制转十进制

二进制如何表示整数?

  • 计算机数据的存储使用二进制补码形式存储,并且最高位是符号位
    • 正数:最高位是0
    • 负数:最高位是1
  • 规 定
    • 正数:原码=反码=补码
    • 负数:
      • 负数的原码:把十进制转为二进制,然后最高位设置为1
      • 负数的反码:在原码的基础上,最高位不变,其余位取反(0变1,1变0)
      • 负数的补码:反码+1

二进制转十进制:权相加法

针对于byte数据举例来说:

image-20220523114701881

  • 例如:byte类型(1个字节,8位)

    25 ==> 原码 0001 1001 ==> 反码 0001 1001 –>补码 0001 1001

    -25 ==>原码 1001 1001 ==> 反码1110 0110 ==>补码 1110 0111

1
2
3
4
5
6
7
8
整数:
正数:25 00000000 00000000 000000000 00011001(原码)
正数:25 00000000 00000000 000000000 00011001(反码)
正数:25 00000000 00000000 000000000 00011001(补码)

负数:-25 10000000 00000000 000000000 00011001(原码)
负数:-25 11111111 11111111 111111111 11100110(反码)
负数:-25 11111111 11111111 111111111 11100111(补码)

一个字节可以存储的整数范围是多少?

1
2
3
4
5
6
7
8
9
//1个字节:8位

0000 0001 ~ 0111 111 ==> 1~127

1000 0001 ~ 1111 1111 ==> -127 ~ -1

0000 0000 ==>0

1000 0000 ==> -128(特殊规定)=-127-1

6.5 十进制转二进制

十进制转二进制:除2取余的逆

image-20220312000042595

6.6 二进制与八进制、十六进制间的转换

二进制转八进制(三合一)

image-20220312000233863

二进制转十六进制(四合一)

image-20220312000251113

八进制、十六进制转二进制(一拆多)

image-20220312000341297

6.7 各进制间的转换

image-20220312000143438

练习:

1
2
3
4
5
6
7
8
9
1.将以下十进制数转换为十六进制和二进制
十进制: 123 256 87 62
十六进制: 7b 100 57 3e 从走往左四合一,不够补0
二进制: 1111011 100000000 1010111 111110 除2取余反向读

2.将以下十六进制数转换为十进制和二进制
十六进制: 0x123 0x25F 0x38 0x62
十进制: 291 607 56 98 拆开按照16^算
二进制: 100100011 1001011111 111000 1100010 一拆四 到时候最前面的0记得删除!!!

7. 运算符(Operator)(掌握)

运算符是一种特殊的符号,用以表示数据的运算、赋值和比较等。

运算符的分类:

  • 按照功能分为:算术运算符、赋值运算符、比较(或关系)运算符、逻辑运算符、位运算符、条件运算符、Lambda运算符
分类 运算符
算术运算符(7个) +、-、*、/、%、++、–
赋值运算符(12个) =、+=、-=、*=、/=、%=、>>=、<<=、>>>=、&=、|=、^=等
比较(或关系)运算符(6个) >、>=、<、<=、==、!=
逻辑运算符(6个) &、|、^、!、&&、||
位运算符(7个) &、|、^、~、<<、>>、>>>
条件运算符(1个) (条件表达式)?结果1:结果2
Lambda运算符(1个) ->(第18章时讲解)
  • 按照操作数个数分为:一元运算符(单目运算符)、二元运算符(双目运算符)、三元运算符 (三目运算符)
分类 运算符
一元运算符(单目运算符) 正号(+)、负号(-)、++、–、!、~
二元运算符(双目运算符) 除了一元和三元运算符剩下的都是二元运算符
三元运算符 (三目运算符) (条件表达式)?结果1:结果2

7.1 算术运算符

7.1.1 基本语法

image-20220312000848332

举例1:加减乘除模

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ArithmeticTest1 {
public static void main(String[] args) {
int a = 3;
int b = 4;

System.out.println(a + b);// 3+4=7
System.out.println(a - b);// 3-4=-1
System.out.println(a * b);// 3*4=12
System.out.println(a / b);// 3/4=0
System.out.println(a % b);// 3%4=3 取余为3

//重点:结果与被模数符号相同
System.out.println(5%2);//1 被模数为5
System.out.println(5%-2);//1 被模数为5
System.out.println(-5%2);//-1 被模数为-5
System.out.println(-5%-2);//-1 被模数为-5
//商*除数 + 余数 = 被除数
//5%-2 ==>商是-2,余数时1 (-2)*(-2)+1 = 5
//-5%2 ==>商是-2,余数是-1 (-2)*2+(-1) = -4-1=-5
}
}

举例2:“+”号的两种用法

  • 第一种:对于+两边都是数值的话,+就是加法的意思 —– 数值+数值=数值
  • 第二种:对于+两边至少有一边是字符串的话,+就是拼接的意思 —– 字符串+数值=字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ArithmeticTest2 {
public static void main(String[] args) {
// 字符串类型的变量基本使用
// 数据类型 变量名称 = 数据值;
String str1 = "Hello";
System.out.println(str1); // Hello

System.out.println("Hello" + "World"); // HelloWorld

String str2 = "Java";
// String + int --> String
System.out.println(str2 + 520); // Java520
// String + int + int
// String + int
// String
System.out.println(str2 + 5 + 20); // Java520
}
}

举例3:自加自减运算

理解:++ 运算,表示自增1。同理,-- 运算,表示自减1,用法与++ 一致。

1、单独使用

  • 变量在单独运算的时候,变量前++和变量后++,是没有区别的。
  • 变量前++ :例如 ++a
  • 变量后++ :例如 a++
1
2
3
4
5
6
7
8
9
10
public class ArithmeticTest3 {
public static void main(String[] args) {
// 定义一个int类型的变量a
int a = 3;
//++a;
a++;
// 无论是变量前++还是变量后++,结果都是4
System.out.println(a);
}
}

2、复合使用

  • 其他变量放在一起使用或者和输出语句放在一起使用前++后++就产生了不同。
  • 变量前++ :变量先自增1,然后再运算。
  • 变量后++ :变量先运算,然后再自增1。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ArithmeticTest4 {
public static void main(String[] args) {
// 其他变量放在一起使用
int x = 3;
//int y = ++x; // y的值是4,x的值是4,
int y = x++; // y的值是3,x的值是4

System.out.println(x);
System.out.println(y);
System.out.println("==========");

// 和输出语句一起
int z = 5;
//System.out.println(++z);// 输出结果是6,z的值也是6
System.out.println(z++);// 输出结果是5,z的值是6
System.out.println(z);

}
}

7.1.2 案例与练习

案例1:

1
2
3
4
5
6
7
8
9
10
11
12
随意给出一个整数,打印显示它的个位数,十位数,百位数的值。
格式如下:
数字xxx的情况如下:
个位数:
十位数:
百位数:

例如:
数字153的情况如下:
个位数:3
十位数:5
百位数:1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @author 尚硅谷-宋红康
* @create 12:20
*/
class ArithmeticExer1 {
public static void main(String[] args) {
//随意给出一个整数,打印显示它的个位数,十位数,百位数的值。
// 主打一个缩小范围然后取余确定最后一位
int n=1234;
int qian=n/1000;
int bai=n/100%10; // 提示:n/100=12
int shi=n/10%10; //提示:n/100=123
int ge=n%10;
System.out.println("个位数:"+ge);
System.out.println("十位数:"+shi);
System.out.println("百位数:"+bai);
System.out.println("千位数:"+qian);

}
}

拓展:获取一个四位数的个位,十位,百位,千位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @author 尚硅谷-宋红康
* @create 12:39
*/
public class ArithmeticExer01 {
public static void main (String [] args) {
// 获取一个四位数的个位,十位,百位,千位
// 主打一个缩小范围然后取余确定最后一位
int n=1234;
int qian=n/1000;
int bai=n/100%10; // 提示:n/100=12
int shi=n/10%10; //提示:n/100=123
int ge=n%10;
System.out.println("个位数:"+ge);
System.out.println("十位数:"+shi);
System.out.println("百位数:"+bai);
System.out.println("千位数:"+qian);

}
}

案例2:为抵抗洪水,战士连续作战89小时,编程计算共多少天零多少小时?

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @author 尚硅谷-宋红康
* @create 17:47
*/
public class ArithmeticExer2 {
public static void main(String[] args){
//为抵抗洪水,战士连续作战89小时,编程计算共多少天零多少小时?
int sum=89;
int day=89/24;
int hour=89%24;
System.out.println("一共"+day+"天"+hour+"小时");
}
}

练习1:算术运算符:自加、自减

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ArithmeticExer3{
public static void main(String[] args){
int i1 = 10;
int i2 = 20;
int i = i1++; // 此时 i=10 i1=11
System.out.print("i="+i); // i=10
System.out.println("i1="+i1);// i1=11
i = ++i1; // 此时i1=12 i=12
System.out.print("i="+i);// i=12
System.out.println("i1="+i1);// i1=12
i = i2--; // 此时i2=19 i=20
System.out.print("i="+i);// i=20
System.out.println("i2="+i2);// i2=19
i = --i2; // 此时i2=18 i=18
System.out.print("i="+i);// i=18
System.out.println("i2="+i2);// i2=18
}
}

练习2:

1
System.out.println("5+5=" + 5 + 5); //打印结果是? 5+5=55   因为string+int+int => string+int => string

练习3:

1
2
3
byte bb1 = 127;
bb1++;
System.out.println("bb1 = " + bb1);//-128

练习4:

1
2
3
int i = 1;
int j = i++ + ++i * i++; //从左往右 1+3*3=10 最后i等于4
System.out.println("j = " + j);

练习5:(企业真题)写出下列程序的输出结果

1
2
3
4
5
6
7
int i = 2;
int j = i++;
System.out.println(j);

int m = 2;
m = m++; //(1)先取b的值“2”放操作数栈 (2)m再自增,m=3 (3)再把操作数栈中的"2"赋值给m,m=2
System.out.println(m); // 是2!!!!

7.2 赋值运算符

7.2.1 基本语法

  • 符号:=

    • 当“=”两侧数据类型不一致时,可以使用自动类型转换或使用强制类型转换原则进行处理。
    • 支持连续赋值(先定义好之后才能连续赋值)。
  • 扩展赋值运算符: +=、 -=、*=、 /=、%=

    赋值运算符 符号解释
    += 将符号左边的值右边的值进行相加操作,最后将结果赋值给左边的变量
    -= 将符号左边的值右边的值进行相减操作,最后将结果赋值给左边的变量
    *= 将符号左边的值右边的值进行相乘操作,最后将结果赋值给左边的变量
    /= 将符号左边的值右边的值进行相除操作,最后将结果赋值给左边的变量
    %= 将符号左边的值右边的值进行取余操作,最后将结果赋值给左边的变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class SetValueTest1 {
public static void main(String[] args) {
int i1 = 10;
long l1 = i1; //自动类型转换

byte bb1 = (byte)i1; //强制类型转换

int i2 = i1;

//连续赋值的测试
//以前的写法
int a1 = 10;
int b1 = 10;

//连续赋值的写法
int a2,b2;
a2 = b2 = 10;

int a3 = 10,b3 = 20;

//举例说明+= -= *= /= %=
int m1 = 10;
m1 += 5; //类似于 m1 = m1 + 5的操作,但不等同于。
System.out.println(m1);//15

//练习1:开发中,如何实现一个变量+2的操作呢?
// += 的操作不会改变变量本身的数据类型。其他拓展的运算符也如此!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//写法1:推荐
short s1 = 10;
s1 += 2; //编译通过,因为在得到int类型的结果后,JVM自动完成一步强制类型转换,将int类型强转成short
System.out.println(s1);//12
//写法2:
short s2 = 10;
//s2 = s2 + 2;//编译报错,因为将int类型的结果赋值给short类型的变量s时,可能损失精度
s2 = (short)(s2 + 2);
System.out.println(s2);


//练习2:开发中,如何实现一个变量+1的操作呢?
//写法1:推荐
int num1 = 10;
num1++;
System.out.println(num1);

//写法2:
int num2 = 10;
num2 += 1;
System.out.println(num2);

//写法3:
int num3 = 10;
num3 = num3 + 1;
System.out.println(num3);

}
}

7.2.2 练习

练习1:

1
2
3
short s = 3; 
s = s+2; //① 编译报错 s是一个short short+int=int不能直接给short 需要强制类型转换 s=(short)(s+2);
s += 2; //② 正常执行 short+int=int 自动完成了强制类型转换(+=不改变值的数据类型)

练习2:

1
2
3
4
5
int i = 1;
i *= 0.1;
System.out.println(i);// 因为*=不改变值的数据类型 0.1就截断为0 [脑子短路]
i++;
System.out.println(i);//1

练习3:

1
2
3
4
5
int m = 2;
int n = 3;
n *= m++; // 可以看作是 n = n * m++; n=3*2=6 m=3
System.out.println("m=" + m);//3
System.out.println("n=" + n);//6

练习4:

1
2
3
int n = 10;
n += (n++) + (++n); // 可以看作是 n = n + (n++) + (++n)=10+10+12=32
System.out.println(n);//32

练习5:你有几种办法实现变量值减1?变量值减2呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @author 尚硅谷-宋红康
* @create 16:55
*/
public class MinusTest {
public static void main(String[] args) {
//练习①:变量值减1
short s = 10;
//方式1:
//s = (short)(s - 1);
//方式2:推荐
s--; //或者 --s
//方式3:
s -= 1;

//练习②:变量值减2
short s1 = 10;
//方式1:
//s1 = (short)(s1 - 2);
//方式2:推荐
s1 -= 2;
}
}

7.3 比较(关系)运算符

image-20220312001742263

  • 比较运算符的结果都是boolean型,结果只有true/false。

  • > < >= <= :只适用于基本数据类型(除boolean类型之外)

    == != :适用于基本数据类型和引用数据类型

  • 比较运算符“==”不能误写成“=

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class CompareTest {
public static void main(String[] args) {
int i1 = 10;
int i2 = 20;

System.out.println(i1 == i2);//false
System.out.println(i1 != i2);//true
System.out.println(i1 >= i2);//false


int m = 10;
int n = 20;
System.out.println(m == n);//false
System.out.println(m = n);//20

boolean b1 = false;
boolean b2 = true;
System.out.println(b1 == b2);//false
System.out.println(b1 = b2);//true
}
}

思考:

1
2
3
4
5
6
boolean b1 = false;
//区分好==和=的区别。
if(b1 == true) //if(b1 = true)
System.out.println("结果为真");
else
System.out.println("结果为假");

7.4 逻辑运算符

7.4.1 基本语法

image-20220312001943403
  • 逻辑运算符,boolean 逻辑运算符 boolean =boolean 。

  • 运算符说明:

    • & 和 &&:表示”且”关系,当符号左右两边布尔值都是true时,结果才能为true。否则,为false。
    • | 和 || :表示”或”关系,当符号两边布尔值有一边为true时,结果为true。当两边都为false时,结果为false。
    • ! :表示”非”关系,当变量布尔值为true时,结果为false。当变量布尔值为false时,结果为true。
    • ^ :当符号左右两边布尔值不同时,结果为true。当两边布尔值相同时,结果为false。
      • 理解:异或,追求的是“异”!
  • 逻辑运算符用于连接布尔型表达式,在Java中不可以写成 3 < x < 6,应该写成x > 3 & x < 6 。

  • 区分“&”和“&&”:

    • 相同点:如果符号左边是true,则二者都执行符号右边的操作

    • 不同点:& : 如果符号左边是false,则继续执行符号右边的操作

    ​ && :如果符号左边是false,则不再继续执行符号右边的操作(有一个不符合可以跳过)

    • 建议:开发中,推荐使用 &&
  • 区分“|”和“||”:

- 相同点:如果符号左边是false,则二者都执行符号右边的操作



- 不同点:| : 如果符号左边是true,则继续执行符号右边的操作

  ​               || :如果符号左边是true,则不再继续执行符号右边的操作(有一个符合可以跳过)



- 建议:开发中,推荐使用 ||

代码举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class LoginTest {
public static void main(String[] args) {
int a = 3;
int b = 4;
int c = 5;

// & 与,且;有false则false
System.out.println((a > b) & (a > c));
System.out.println((a > b) & (a < c));
System.out.println((a < b) & (a > c));
System.out.println((a < b) & (a < c));
System.out.println("===============");
// | 或;有true则true
System.out.println((a > b) | (a > c));
System.out.println((a > b) | (a < c));
System.out.println((a < b) | (a > c));
System.out.println((a < b) | (a < c));
System.out.println("===============");
// ^ 异或;相同为false,不同为true 追求异!!!
System.out.println((a > b) ^ (a > c));
System.out.println((a > b) ^ (a < c));
System.out.println((a < b) ^ (a > c));
System.out.println((a < b) ^ (a < c));
System.out.println("===============");
// ! 非;非false则true,非true则false
System.out.println(!false);
System.out.println(!true);

//&和&&的区别
System.out.println((a > b) & (a++ > c));
System.out.println("a = " + a);
System.out.println((a > b) && (a++ > c));
System.out.println("a = " + a);
System.out.println((a == b) && (a++ > c));
System.out.println("a = " + a);

//|和||的区别
System.out.println((a > b) | (a++ > c));
System.out.println("a = " + a);
System.out.println((a > b) || (a++ > c));
System.out.println("a = " + a);
System.out.println((a == b) || (a++ > c));
System.out.println("a = " + a);
}
}

7.4.2 案例与练习

案例:

1
2
3
4
5
6
7
1. 定义类 CompareLogicExer
2. 定义 main方法
3. 定义一个int类型变量a,变量b,都赋值为20
4. 定义boolean类型变量bo1 , 判断++a 是否被3整除,并且a++ 是否被7整除,将结果赋值给bo1
5. 输出a的值,bo1的值
6. 定义boolean类型变量bo2 , 判断b++ 是否被3整除,并且++b 是否被7整除,将结果赋值给bo2
7. 输出b的值,bo2的值
1
2
3
4
5
6
7
8
9
10
11
12
public class CompareLogicExer {
public static void main(String[] args) {
int a=20;
int b=20;
boolean b01=(++a%3==0)&&(a++%7==0);
System.out.println(a); // a=22 先是++a之后成为21 最后a++成为22
System.out.println(b01); // true
boolean b02=(b++%3==0)&&(++b%7==0); //一定要记得&&是可以跳出来循环的!
System.out.println(b); // b=21 先是b++之后成为21 直接&&不成立直接跳出来了!!!!
System.out.println(b02); // false
}
}

练习1:区分 & 和 && (&&被推荐使用,因为有一个不符合就跳出判断了)

1
2
3
4
5
6
7
int x = 1;
int y = 1;

if(x++ == 2 & ++y == 2){ // 1==2 不成立但是x=2 然后&不能跳过循环继续y=2 2==2成立 但是最终是false&true=false
x = 7;
}
System.out.println("x=" + x + ",y=" + y); // x=2 y=2
1
2
3
4
5
6
int x = 1,y = 1;

if(x++ == 2 && ++y == 2){ // 1==2不成立 x=2 因为是&&直接跳出循环
x =7;
}
System.out.println("x="+x+",y="+y); // x=2 y=1

练习2:区分 | 和 ||(||被推荐使用,因为有一个符合就跳出判断了)

1
2
3
4
5
6
int x = 1,y = 1;

if(x++==1 | ++y==1){ // 1==1 x=2 左边成立 y=2 2==1不成立 true|false=true
x =7;
}
System.out.println("x="+x+",y="+y); // x=7 y=2
1
2
3
4
5
6
int x = 1,y = 1;

if(x++==1 || ++y==1){ // 1==1 左边成立 x=2 直接成功
x =7;
}
System.out.println("x="+x+",y="+y); // x=7 y=1

练习3:程序输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class  Test  {
public static void main (String [] args) {
boolean x = true;
boolean y = false;
short z = 42;

if ((z++ == 42) && (y = true)) { // 42==42 左边成立 z=43 y=true
z++; // z=44
}
if ((x = false) || (++z == 45)) { // x=false 左边成立 z=45 45==45 不成立
z++; // z=46
}

System.out.println("z=" + z); // z=46
}
}
1
2
//结果为:
//z= 46

7.5 位运算符(难点、非重点)

7.5.1 基本语法

image-20220313174721111 image-20220312002506339
  • 位运算符的运算过程都是基于二进制的补码运算

(1)左移:<<

运算规则:在一定范围内,数据每向左移动n位,相当于原数据*2^n。(正数、负数都适用)

【注意】当左移的位数n超过该数据类型的总位数时,相当于左移(n-总位数)位

1
3<<4  类似于  3*2^4 => 3*16 => 48

image-20200225113651675

1
-3<<4  类似于  -3*2^4 => -3*16 => -48

image-20200225114707524

(2)右移:>>

运算规则:在一定范围内,数据每向右移动n位,相当于原数据/(2^n)。(正数、负数都适用)

【注意】如果不能整除,向下取整

1
69>>4  类似于  69/(2^4) = 69/16 =4

image-20200225115636844

1
-69>>4  类似于  -69/(2^4) = -69/16 = -5

image-20200225120112188

(3)无符号右移:>>>

运算规则:往右移动后,左边空出来的位直接补0。(正数、负数都适用)

1
69>>>4  类似于  69/2的4次 = 69/16 =4

image-20200225121104734

1
-69>>>4   结果:268435451

image-20200225121244290

(4)按位与:&

运算规则:对应位都是1才为1,否则为0

  • 1 & 1 结果为1

  • 1 & 0 结果为0

  • 0 & 1 结果为0

  • 0 & 0 结果为0
1
9 & 7 = 1

image-20200225122440953

1
-9 & 7 = 7

image-20200225122221616

(5)按位或:|

运算规则:对应位只要有1即为1,否则为0

  • 1 | 1 结果为1

  • 1 | 0 结果为1

  • 0 | 1 结果为1

  • 0 & 0 结果为0

1
9 | 7  //结果: 15

image-20200225122758851

1
-9 | 7 //结果: -9

image-20200225123409130

(6)按位异或:^

运算规则:对应位一个为1一个为0,才为1,否则为0(找异!)

  • 1 ^ 1 结果为0

  • 1 ^ 0 结果为1

  • 0 ^ 1 结果为1

  • 0 ^ 0 结果为0
1
9 ^ 7  //结果为14

image-20200225123445305

1
-9 ^ 7 //结果为-16

image-20200225133145727

(7)按位取反:~

运算规则:对应位为1,则结果为0;对应位为0,则结果为1

  • ~0就是1

  • ~1就是0

1
~9  //结果:-10

image-20200225124112662

1
~-9  //结果:8

image-20200225124156862

7.5.2 举例

举例1:

snipaste_20220312_002549

举例2:体会 m = k ^ n = (m ^ n) ^ n

image-20220312002736157

7.5.3 案例

案例1:高效的方式计算2 * 8的值(经典面试题)

1
答案:2 << 3 、  8  << 1

案例2:如何交换两个int型变量的值?String呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* @author 尚硅谷-宋红康
* @create 16:58
*/
public class BitExer {
public static void main(String[] args) {
int m = 10;
int n = 5;

System.out.println("m = " + m + ", n = " + n);

//(推荐)实现方式1:优点:容易理解,适用于不同数据类型 缺点:需要额外定义变量
//int temp = m;
//m = n;
//n = temp;

//实现方式2:优点:没有额外定义变量 缺点:可能超出int的范围;只能适用于数值类型
m=m+n; //20+10=30 全上来了m+n
n=m-n; //30-20=10 n=10 全部的值-右边原来的=剩下的就是左边的(新的右边的)
m=m-n; //30-20=20 m=20 全部-新的右边的=新的左边的

//实现方式3:优点:没有额外定义变量 缺点:不易理解;只能适用于数值类型
m = m ^ n;
n = m ^ n; //(m ^ n) ^ n
m = m ^ n;

System.out.println("m = " + m + ", n = " + n);
}
}

7.6 条件运算符

7.6.1 基本语法

  • 条件运算符格式:
1
(条件表达式)? 表达式1:表达式2
  • 说明:条件表达式是boolean类型的结果,根据boolean的值选择表达式1或表达式2

    image-20220312002841945
  • 如果运算后的结果赋给新的变量,要求表达式1和表达式2为同种或兼容的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
int i = (1==2 ? 100 : 200);
System.out.println(i);//200

boolean marry = false;
System.out.println(marry ? "已婚" : "未婚" );

double d1 = (m1 > m2)? 1 : 2.0;
System.out.println(d1);

int num = 12;
System.out.println(num > 0? true : "num非正数");
}

7.6.2 案例

案例1:获取两个数中的较大值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @author 尚硅谷-宋红康
* @create 12:40
*/
public class ConditionExer1 {
public static void main(String[] args) {
//获取两个数的较大值
int m1 = 10;
int m2 = 20;

int max1 = (m1 > m2)? m1 : m2;
System.out.println("m1和m2中的较大值为" + max1);
}
}

案例2:获取三个数中的最大值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @author 尚硅谷-宋红康
* @create 12:43
*/
public class ConditionExer2 {
public static void main(String[] args) {
int a=13;
int b=4442;
int c=54;
int secondmax=a>b?a:b; //a和b里面的最大数
int max=secondmax>c?secondmax:c; //和c再比较一下
System.out.println(max);
}
}

案例3:今天是周2,10天以后是周几?

要求:控制台输出”今天是周2,10天以后是周x”。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @author 尚硅谷-宋红康
* @create 12:46
*/
public class ConditionExer3 {

public static void main(String[] args) {
int day=2;
int tomorrow=2+10%7; // 10/7相当于过了几周 现在意思就是10%7就是整周以外过了几天 不用管整7!!
System.out.println(tomorrow);
}

}

7.6.3 与if-else的转换关系

  • 条件运算符——->if-else结构。
  • 开发中,如果既可以使用条件运算符(执行效率高,推荐!),又可以使用if-else。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//if-else实现获取两个数的较大值

int i1 = 10;
int i2 = 20;

int max;//声明变量max,用于记录i1和i2的较大值

if(i1 > i2){
max = i1;
}else{
max = i2;
}

System.out.println(max);

7.7 运算符优先级

运算符有不同的优先级,所谓优先级就是在表达式运算中的运算符顺序。

上一行中的运算符总是优先于下一行的。

优先级 运算符说明 Java运算符
1 括号 ()[]{}
2 正负号 +-
3 单元运算符 ++--~
4 乘法、除法、求余 */%
5 加法、减法 +-
6 移位运算符 <<>>>>>
7 关系运算符 <<=>=>instanceof
8 等价运算符 ==!=
9 按位与 &
10 按位异或 ^
11 按位或 `
12 条件与 &&
13 条件或 `
14 三元运算符 ? :
15 赋值运算符 =+=-=*=/=%=
16 位赋值运算符 &=、`

开发建议:

  1. 不要过多的依赖运算的优先级来控制表达式的执行顺序,这样可读性太差,尽量使用()来控制表达式的执行顺序。
  2. 不要把一个表达式写得过于复杂,如果一个表达式过于复杂,则把它分成几步来完成。例如:
    ​ (num1 + num2) * 2 > num3 && num2 > num3 ? num3 : num1 + num2;

8. 【拓展】关于字符集

8.1 字符集

  • 编码与解码

计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。按照某种规则,将字符存储到计算机中,称为编码 。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为解码

  • 字符编码(Character Encoding) : 就是一套自然语言的字符与二进制数之间的对应规则。

  • 字符集:也叫编码表。是一个系统支持的所有字符的集合,包括各国家文字、标点符号、图形符号、数字等。

8.2 ASCII码

  • ASCII码(American Standard Code for Information Interchange,美国信息交换标准代码):上个世纪60年代,美国制定了一套字符编码,对英语字符与二进制位之间的关系,做了统一规定。这被称为ASCII码。
  • ASCII码用于显示现代英语,主要包括控制字符(回车键、退格、换行键等)和可显示字符(英文大小写字符、阿拉伯数字和西文符号)。
  • 基本的ASCII字符集,使用7位(bits)表示一个字符(最前面的1位统一规定为0),共128个字符。比如:空格“SPACE”是32(二进制00100000),大写的字母A是65(二进制01000001)。
  • 缺点:不能表示所有字符。

image-20220513095907601

8.3 ISO-8859-1字符集

  • 拉丁码表,别名Latin-1,用于显示欧洲使用的语言,包括荷兰语、德语、意大利语、葡萄牙语等
  • ISO-8859-1使用单字节编码,兼容ASCII编码。

8.4 GBxxx字符集

  • GB就是国标的意思,是为了显示中文而设计的一套字符集。
  • GB2312:简体中文码表。一个小于127的字符的意义与原来相同,即向下兼容ASCII码。但两个大于127的字符连在一起时,就表示一个汉字,这样大约可以组合了包含7000多个简体汉字,此外数学符号、罗马希腊的字母、日文的假名们都编进去了,这就是常说的”全角”字符,而原来在127号以下的那些符号就叫”半角”字符了。
  • GBK:最常用的中文码表。是在GB2312标准基础上的扩展规范,使用了双字节编码方案,共收录了21003个汉字,完全兼容GB2312标准,同时支持繁体汉字以及日韩汉字等。
  • GB18030:最新的中文码表。收录汉字70244个,采用多字节编码,每个字可以由1个、2个或4个字节组成。支持中国国内少数民族的文字,同时支持繁体汉字以及日韩汉字等。

8.5 Unicode码

  • Unicode编码为表达任意语言的任意字符而设计,也称为统一码、标准万国码。Unicode 将世界上所有的文字用2个字节统一进行编码,为每个字符设定唯一的二进制编码,以满足跨语言、跨平台进行文本处理的要求。

  • Unicode 的缺点:这里有三个问题:

    • 第一,英文字母只用一个字节表示就够了,如果用更多的字节存储是极大的浪费
    • 第二,如何才能区别Unicode和ASCII?计算机怎么知道两个字节表示一个符号,而不是分别表示两个符号呢?
    • 第三,如果和GBK等双字节编码方式一样,用最高位是1或0表示两个字节和一个字节,就少了很多值无法用于表示字符,不够表示所有字符
  • Unicode在很长一段时间内无法推广,直到互联网的出现,为解决Unicode如何在网络上传输的问题,于是面向传输的众多 UTF(UCS Transfer Format)标准出现。具体来说,有三种编码方案,UTF-8、UTF-16和UTF-32。

8.6 UTF-8

  • Unicode是字符集,UTF-8、UTF-16、UTF-32是三种将数字转换到程序数据的编码方案。顾名思义,UTF-8就是每次8个位传输数据,而UTF-16就是每次16个位。其中,UTF-8 是在互联网上使用最广的一种 Unicode 的实现方式。
  • 互联网工程工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码。所以,我们开发Web应用,也要使用UTF-8编码。UTF-8 是一种变长的编码方式。它可以使用 1-4 个字节表示一个符号它使用一至四个字节为每个字符编码,编码规则:
    1. 128个US-ASCII字符,只需一个字节编码。
    2. 拉丁文等字符,需要二个字节编码。
    3. 大部分常用字(含中文),使用三个字节编码。
    4. 其他极少使用的Unicode辅助字符,使用四字节编码。
  • 举例

Unicode符号范围 | UTF-8编码方式

1
2
3
4
5
6
7
8
9
10
11
(十六进制)           | (二进制)

————————————————————|—–—–—–—–—–—–—–—–—–—–—–—–—–—–

0000 0000-0000 007F | 0xxxxxxx(兼容原来的ASCII)

0000 0080-0000 07FF | 110xxxxx 10xxxxxx

0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx

0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

image-20220525164636164

8.7 小结

字符集

注意:在中文操作系统上,ANSI(美国国家标准学会、AMERICAN NATIONAL STANDARDS INSTITUTE: ANSI)编码即为GBK;在英文操作系统上,ANSI编码即为ISO-8859-1。

第03章_流程控制语句


本章专题与脉络

第1阶段:Java基本语法-第03章
  • 流程控制语句是用来控制程序中各语句执行顺序的语句,可以把语句组合成能完成一定功能的小逻辑模块。

  • 程序设计中规定的三种流程结构,即:

    • 顺序结构
      • 程序从上到下逐行地执行,中间没有任何判断和跳转。
    • 分支结构
      • 根据条件,选择性地执行某段代码。
      • if…elseswitch-case两种分支语句。
    • 循环结构
      • 根据循环条件,重复性的执行某段代码。
      • forwhiledo-while三种循环语句。
      • 补充:JDK5.0 提供了foreach循环,方便的遍历集合、数组元素。(第12章集合中讲解)
  • 生活中、工业生产中流程控制举例

洗衣流程

1. 顺序结构

顺序结构就是程序从上到下逐行地执行。表达式语句都是顺序执行的。并且上一行对某个变量的修改对下一行会产生影响。

image-20211218093256771

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class StatementTest{
public static void main(String[] args){
int x = 1;
int y = 2;
System.out.println("x = " + x);
System.out.println("y = " + y);
//对x、y的值进行修改
x++;
y = 2 * x + y;
x = x * 10;
System.out.println("x = " + x);
System.out.println("y = " + y);
}
}

Java中定义变量时采用合法的前向引用。如:

1
2
3
4
public static void main(String[] args) {
int num1 = 12;
int num2 = num1 + 2;
}

错误形式:

1
2
3
4
public static void main(String[] args) {
int num2 = num1 + 2;
int num1 = 12;
}

2. 分支语句

2.1 if-else条件判断结构

2.1.1 基本语法

结构1:单分支条件判断:if

格式:

1
2
3
if(条件表达式){
语句块;

说明:条件表达式必须是布尔表达式(关系表达式或逻辑表达式)/ 布尔变量。

执行流程:

  1. 首先判断条件表达式是true/false
  2. 如果是true就执行语句块
  3. 如果是false就不执行语句块

image-20220514160139926

结构2:双分支条件判断:if…else

格式:

1
2
3
4
5
6
if(条件表达式) { 
语句块1;
}
else {
语句块2;
}

执行流程:

  1. 首先判断条件表达式看其结果是true还是false
  2. 如果是true就执行语句块1
  3. 如果是false就执行语句块2

image-20220514160243421

结构3:多分支条件判断:if…else if…else

格式:

1
2
3
4
5
6
7
8
9
10
11
if (条件表达式1) {
语句块1;
} else if (条件表达式2) {
语句块2;
}
...
}else if (条件表达式n) {
语句块n;
} else {
语句块n+1;
}

说明:一旦条件表达式为true,则进入执行相应的语句块。执行完对应的语句块之后,就跳出当前结构。

执行流程:

  1. 首先判断关系表达式1看其结果是true还是false
  2. 如果是true就执行语句块1,然后结束当前多分支
  3. 如果是false就继续判断关系表达式2看其结果是true还是false
  4. 如果是true就执行语句块2,然后结束当前多分支
  5. 如果是false就继续判断关系表达式…看其结果是true还是false

​ …

n. 如果没有任何关系表达式为true,就执行语句块n+1,然后结束当前多分支。

image-20220514160538651

2.1.2 应用举例

案例1:成年人心率的正常范围是每分钟60-100次。体检时,如果心率不在此范围内,则提示需要做进一步的检查。

1
2
3
4
5
6
7
8
9
10
11
public class IfElseTest1 {
public static void main(String[] args){
int heartBeats = 89;

if(heartBeats < 60 || heartBeats > 100){
System.out.println("你需要做进一步的检查");
}

System.out.println("体检结束");
}
}

案例2:定义一个整数,判定是偶数还是奇数

1
2
3
4
5
6
7
8
9
10
11
public class IfElseTest2 {
public static void main(String[] args){
int a = 10;

if(a % 2 == 0) {
System.out.println(a + "是偶数");
} else{
System.out.println(a + "是奇数");
}
}
}

案例3:

1
2
3
4
5
6
7
8
岳小鹏参加Java考试,他和父亲岳不群达成承诺:
如果:
成绩为100分时,奖励一辆跑车;
成绩为(80,99]时,奖励一辆山地自行车;
当成绩为[60,80]时,奖励环球影城一日游;
其它时,胖揍一顿。

说明:默认成绩是在[0,100]范围内
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class IfElseTest3 {
public static void main(String[] args) {

int score = 67;//岳小鹏的期末成绩
//写法一:默认成绩范围为[0,100]
if(score == 100){
System.out.println("奖励一辆跑车");
}else if(score > 80 && score <= 99){ //错误的写法:}else if(80 < score <= 99){
System.out.println("奖励一辆山地自行车");
}else if(score >= 60 && score <= 80){
System.out.println("奖励环球影城玩一日游");
}
//else{
// System.out.println("胖揍一顿");
//}


//写法二:
// 默认成绩范围为[0,100]
if(score == 100){
System.out.println("奖励一辆跑车");
}else if(score > 80){
System.out.println("奖励一辆山地自行车");
}else if(score >= 60){
System.out.println("奖励环球影城玩一日游");
}else{
System.out.println("胖揍一顿");
}
}
}

image-20221027185234087

image-20221027185500389

当条件表达式之间是“互斥”关系时(即彼此没有交集),条件判断语句及执行语句间顺序无所谓。

当条件表达式之间是“包含”关系时,“小上大下 / 子上父下”,否则范围小的条件表达式将不可能被执行。

2.1.3 if…else嵌套

在 if 的语句块中,或者是在else语句块中,又包含了另外一个条件判断(可以是单分支、双分支、多分支),就构成了嵌套结构

执行的特点:
(1)如果是嵌套在if语句块中的,只有当外部的if条件满足,才会去判断内部的条件
(2)如果是嵌套在else语句块中的,只有当外部的if条件不满足,进入else后,才会去判断内部的条件

案例4:由键盘输入三个整数分别存入变量num1、num2、num3,对它们进行排序(使用 if-else if-else),并且从小到大输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class IfElseTest4 {
public static void main(String[] args) {

//声明num1,num2,num3三个变量并赋值
int num1 = 23,num2 = 32,num3 = 12;

if(num1 >= num2){

if(num3 >= num1)
System.out.println(num2 + "-" + num1 + "-" + num3);
else if(num3 <= num2)
System.out.println(num3 + "-" + num2 + "-" + num1);
else
System.out.println(num2 + "-" + num3 + "-" + num1);
}else{ //num1 < num2

if(num3 >= num2){
System.out.println(num1 + "-" + num2 + "-" + num3);
}else if(num3 <= num1){
System.out.println(num3 + "-" + num1 + "-" + num2);
}else{
System.out.println(num1 + "-" + num3 + "-" + num2);
}
}
}
}

2.1.4 其它说明

  • 语句块只有一条执行语句时,一对{}可以省略,但建议保留
  • 当if-else结构是“多选一”时,最后的else是可选的,根据需要可以省略

2.1.5 练习

练习1:

1
2
3
4
5
6
7
8
9
//1)对下列代码,若有输出,指出输出结果。
int x = 4;
int y = 1;
if (x > 2) {
if (y > 2)
System.out.println(x + y);
System.out.println("atguigu");
} else
System.out.println("x is " + x);

练习2:

1
2
3
4
5
6
7
8
9
10
boolean b = true;
//如果写成if(b=false)能编译通过吗?如果能,结果是?
if(b == false) //建议:if(!b)
System.out.println("a");
else if(b)
System.out.println("b");
else if(!b)
System.out.println("c");
else
System.out.println("d");

练习3:

定义两个整数,分别为small 和 big,如果第一个整数small大于第二个整数big,就交换。输出显示small和big变量的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class IfElseExer3 {
public static void main(String[] args) {
int small = 10;
int big = 9;

if (small > big) {
int temp = small;
small = big;
big = temp;
}
System.out.println("small=" + small + ",big=" + big);
}
}

练习4:小明参加期末Java考试,通过考试成绩,判断其Java等级,成绩范围[0,100]

  • 90-100 优秀
  • 80-89 好
  • 70-79 良
  • 60-69 及格
  • 60以下 不及格
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.Scanner;
//写法一:
public class IfElseExer4 {
public static void main(String[] args) {
System.out.print("小明的期末Java成绩是:[0,100]");
int score = 89;

if (score < 0 || score > 100) {
System.out.println("你的成绩是错误的");
} else if (score >= 90 && score <= 100) {
System.out.println("你的成绩属于优秀");
} else if (score >= 80 && score < 90) {
System.out.println("你的成绩属于好");
} else if (score >= 70 && score < 80) {
System.out.println("你的成绩属于良");
} else if (score >= 60 && score < 70) {
System.out.println("你的成绩属于及格");
} else {
System.out.println("你的成绩属于不及格");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.Scanner;
//写法二:
public class IfElseExer4 {
public static void main(String[] args) {
System.out.print("小明的期末Java成绩是:[0,100]");
int score = 89;

if (score < 0 || score > 100) {
System.out.println("你的成绩是错误的");
} else if (score >= 90) {
System.out.println("你的成绩属于优秀");
} else if (score >= 80) {
System.out.println("你的成绩属于好");
} else if (score >= 70) {
System.out.println("你的成绩属于良");
} else if (score >= 60) {
System.out.println("你的成绩属于及格");
} else {
System.out.println("你的成绩属于不及格");
}

}
}

练习5:

1
编写程序,声明2个int型变量并赋值。判断两数之和,如果大于等于50,打印“hello world!”
1
2
3
4
5
6
7
8
9
10
public class IfElseExer5 {

public static void main(String[] args) {
int num1 = 12, num2 = 32;

if (num1 + num2 >= 50) {
System.out.println("hello world!");
}
}
}

练习6:

1
编写程序,声明2个double型变量并赋值。判断第一个数大于10.0,且第2个数小于20.0,打印两数之和。否则,打印两数的乘积。
1
2
3
4
5
6
7
8
9
10
11
12
13
public class IfElseExer6 {

public static void main(String[] args) {
double d1 = 21.2,d2 = 12.3;

if(d1 > 10.0 && d2 < 20.0){
System.out.println("两数之和为:" + (d1 + d2));
}else{
System.out.println("两数乘积为:" + (d1 * d2));
}
}

}

练习7:判断水的温度

1
2
3
4
5
6
7
如果大于95℃,则打印“开水”;

如果大于70℃且小于等于95℃,则打印“热水”;

如果大于40℃且小于等于70℃,则打印“温水”;

如果小于等于40℃,则打印“凉水”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class IfElseExer7 {

public static void main(String[] args) {
int waterTemperature = 85;

if(waterTemperature > 95){
System.out.println("开水");
}else if(waterTemperature > 70 && waterTemperature <= 95){
System.out.println("热水");
}else if(waterTemperature > 40 && waterTemperature <= 70){
System.out.println("温水");
}else{
System.out.println("凉水");
}
}

}

2.2 switch-case选择结构

2.2.1 基本语法

语法格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
switch(表达式){
case 常量值1:
语句块1;
//break;
case 常量值2:
语句块2;
//break;
// ...
[default:
语句块n+1;
break;
]
}

执行流程图:

image-20220514101841710

执行过程:

第1步:根据switch中表达式的值,依次匹配各个case。如果表达式的值等于某个case中的常量值,则执行对应case中的执行语句。

第2步:执行完此case的执行语句以后,
​ 情况1:如果遇到break,则执行break并跳出当前的switch-case结构
​ 情况2:如果没有遇到break,则会继续执行当前case之后的其它case中的执行语句。—>case穿透
​ …
​ 直到遇到break关键字或执行完所有的case及default的执行语句,跳出当前的switch-case结构

使用注意点:

  • switch(表达式)中表达式的值必须是下述几种类型之一:byte,short,char,int,枚举 (jdk 5.0),String (jdk 7.0);

  • case子句中的值必须是常量,不能是变量名或不确定的表达式值或范围;

  • 同一个switch语句,所有case子句中的常量值互不相同;

  • break语句用来在执行完一个case分支后使程序跳出switch语句块;

    如果没有break,程序会顺序执行到switch结尾;

  • default子句是可选的。同时,位置也是灵活的。当没有匹配的case时,执行default语句。

2.2.2 应用举例

案例1:

1
2
3
4
5
6
7
8
9
10
11
public class SwitchCaseTest1 {
public static void main(String args[]) {
int num = 1;
switch (num){
case 0:System.out.println("0");break;
case 1:System.out.println("1");break;
case 2:System.out.println("2");break;
default:System.out.println("other");break;
}
}
}

案例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SwitchCaseTest2 {
public static void main(String args[]) {
String season = "summer";
switch (season) {
case "spring":
System.out.println("春暖花开");
break;
case "summer":
System.out.println("夏日炎炎");
break;
case "autumn":
System.out.println("秋高气爽");
break;
case "winter":
System.out.println("冬雪皑皑");
break;
default:
System.out.println("季节输入有误");
break;
}
}
}

错误举例:

1
2
3
4
5
6
7
8
9
10
11
12
int key = 10;
switch(key){
case key > 0 :
System.out.println("正数");
break;
case key < 0:
System.out.println("负数");
break;
default:
System.out.println("零");
break;
}

案例3:使用switch-case实现:对学生成绩大于60分的,输出“合格”。低于60分的,输出“不合格”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class SwitchCaseTest3 {
public static void main(String[] args) {

int score = 67;
/*
写法1:极不推荐
switch(score){
case 0:
System.out.println("不及格");
break;
case 1:
System.out.println("不及格");
break;
//...

case 60:
System.out.println("及格");
break;
//...略...

}
*/

//写法2:
switch(score / 10){
case 0:
case 1:
case 2:
case 3:
case 4:
case 5:
System.out.println("不及格");
break;
case 6:
case 7:
case 8:
case 9:
case 10:
System.out.println("及格");
break;
default:
System.out.println("输入的成绩有误");
break;
}

//写法3:
switch(score / 60){
case 0:
System.out.println("不及格");
break;
case 1:
System.out.println("及格");
break;
default:
System.out.println("输入的成绩有误");
break;
}
}
}

2.2.3 利用case的穿透性

在switch语句中,如果case的后面不写break,将出现穿透现象,也就是一旦匹配成功,不会在判断下一个case的值,直接向后运行,直到遇到break或者整个switch语句结束,执行终止。

案例4:编写程序:从键盘上输入2023年的“month”和“day”,要求通过程序输出输入的日期为2023年的第几天。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import java.util.Scanner;

class SwitchCaseTest4 {
public static void main(String[] args) {

Scanner scan = new Scanner(System.in);

System.out.println("请输入2023年的month:");
int month = scan.nextInt();

System.out.println("请输入2023年的day:");
int day = scan.nextInt();

//这里就不针对month和day进行合法性的判断了,以后可以使用正则表达式进行校验。

int sumDays = 0;//记录总天数

//写法1 :不推荐(存在冗余的数据)
/*
switch(month){
case 1:
sumDays = day;
break;
case 2:
sumDays = 31 + day;
break;
case 3:
sumDays = 31 + 28 + day;
break;
//....

case 12:
//sumDays = 31 + 28 + ... + 30 + day;
break;
}
*/

//写法2:推荐
switch(month){
case 12:
sumDays += 30;//这个30是代表11月份的满月天数
case 11:
sumDays += 31;//这个31是代表10月份的满月天数
case 10:
sumDays += 30;//这个30是代表9月份的满月天数
case 9:
sumDays += 31;//这个31是代表8月份的满月天数
case 8:
sumDays += 31;//这个31是代表7月份的满月天数
case 7:
sumDays += 30;//这个30是代表6月份的满月天数
case 6:
sumDays += 31;//这个31是代表5月份的满月天数
case 5:
sumDays += 30;//这个30是代表4月份的满月天数
case 4:
sumDays += 31;//这个31是代表3月份的满月天数
case 3:
sumDays += 28;//这个28是代表2月份的满月天数
case 2:
sumDays += 31;//这个31是代表1月份的满月天数
case 1:
sumDays += day;//这个day是代表当月的第几天
}

System.out.println(month + "月" + day + "日是2023年的第" + sumDays + "天");
//关闭资源
scan.close();
}
}

拓展:

1
2
3
4
5
6
7
8
从键盘分别输入年、月、日,判断这一天是当年的第几天

注:判断一年是否是闰年的标准:
1)可以被4整除,但不可被100整除

2)可以被400整除

例如:1900,2200等能被4整除,但同时能被100整除,但不能被400整除,不是闰年
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import java.util.Scanner;

public class SwitchCaseTest04 {

public static void main(String[] args) {

Scanner scanner = new Scanner(System.in);
System.out.print("请输入year:");
int year = scanner.nextInt();

System.out.print("请输入month:");
int month = scanner.nextInt();

System.out.print("请输入day:");
int day = scanner.nextInt();

//判断这一天是当年的第几天==>从1月1日开始,累加到xx月xx日这一天
//(1)[1,month-1]个月满月天数
//(2)单独考虑2月份是否是29天(依据是看year是否是闰年)
//(3)第month个月的day天

//声明一个变量days,用来存储总天数
int sumDays = 0;

//累加[1,month-1]个月满月天数
switch (month) {
case 12:
//累加的1-11月
sumDays += 30;//这个30是代表11月份的满月天数
//这里没有break,继续往下走
case 11:
//累加的1-10月
sumDays += 31;//这个31是代表10月的满月天数
//这里没有break,继续往下走
case 10:
sumDays += 30;//9月
case 9:
sumDays += 31;//8月
case 8:
sumDays += 31;//7月
case 7:
sumDays += 30;//6月
case 6:
sumDays += 31;//5月
case 5:
sumDays += 30;//4月
case 4:
sumDays += 31;//3月
case 3:
sumDays += 28;//2月
//在这里考虑是否可能是29天
if (year % 4 == 0 && year % 100 != 0 || year % 400 == 0) {
sumDays++;//多加1天
}
case 2:
sumDays += 31;//1月
case 1:
sumDays += day;//第month月的day天
}

//输出结果
System.out.println(year + "年" + month + "月" + day + "日是这一年的第" + sumDays + "天");

scanner.close();
}
}

案例5:根据指定的月份输出对应季节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import java.util.Scanner;

/*
* 需求:指定一个月份,输出该月份对应的季节。一年有四季:
* 3,4,5 春季
* 6,7,8 夏季
* 9,10,11 秋季
* 12,1,2 冬季
*/
public class SwitchCaseTest5 {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.print("请输入月份:");
int month = input.nextInt();

/*
switch(month) {
case 1:
System.out.println("冬季");
break;
case 2:
System.out.println("冬季");
break;
case 3:
System.out.println("春季");
break;
case 4:
System.out.println("春季");
break;
case 5:
System.out.println("春季");
break;
case 6:
System.out.println("夏季");
break;
case 7:
System.out.println("夏季");
break;
case 8:
System.out.println("夏季");
break;
case 9:
System.out.println("秋季");
break;
case 10:
System.out.println("秋季");
break;
case 11:
System.out.println("秋季");
break;
case 12:
System.out.println("冬季");
break;
default:
System.out.println("你输入的月份有误");
break;
}
*/

// 改进版
switch(month) {
case 1:
case 2:
case 12:
System.out.println("冬季");
break;
case 3:
case 4:
case 5:
System.out.println("春季");
break;
case 6:
case 7:
case 8:
System.out.println("夏季");
break;
case 9:
case 10:
case 11:
System.out.println("秋季");
break;
default:
System.out.println("你输入的月份有误");
break;
}

input.close();
}
}

常见错误实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
switch(month){
case 3|4|5://3|4|5 用了位运算符,11 | 100 | 101结果是 111是7
System.out.println("春季");
break;
case 6|7|8://6|7|8用了位运算符,110 | 111 | 1000结果是1111是15
System.out.println("夏季");
break;
case 9|10|11://9|10|11用了位运算符,1001 | 1010 | 1011结果是1011是11
System.out.println("秋季");
break;
case 12|1|2://12|1|2 用了位运算符,1100 | 1 | 10 结果是1111,是15
System.out.println("冬季");
break;
default:
System.out.println("输入有误");
}

使用if-else实现:

1
2
3
4
5
6
7
8
9
10
11
if ((month == 1) || (month == 2) || (month == 12)) {
System.out.println("冬季");
} else if ((month == 3) || (month == 4) || (month == 5)) {
System.out.println("春季");
} else if ((month == 6) || (month == 7) || (month == 8)) {
System.out.println("夏季");
} else if ((month == 9) || (month == 10) || (month == 11)) {
System.out.println("秋季");
} else {
System.out.println("你输入的月份有误");
}

2.2.4 if-else语句与switch-case语句比较

  • 结论:凡是使用switch-case的结构都可以转换为if-else结构。反之,不成立。

  • 开发经验:如果既可以使用switch-case,又可以使用if-else,建议使用switch-case。因为效率稍高。

  • 细节对比:

    • if-else语句优势
      • if语句的条件是一个布尔类型值,if条件表达式为true则进入分支,可以用于范围的判断,也可以用于等值的判断,使用范围更广
      • switch语句的条件是一个常量值(byte,short,int,char,枚举,String),只能判断某个变量或表达式的结果是否等于某个常量值,使用场景较狭窄
    • switch语句优势
      • 当条件是判断某个变量或表达式是否等于某个固定的常量值时,使用if和switch都可以,习惯上使用switch更多。因为效率稍高。当条件是区间范围的判断时,只能使用if语句。
      • 使用switch可以利用穿透性,同时执行多个分支,而if…else没有穿透性。
  • 案例:只能使用 if-else

    从键盘输入一个整数,判断是正数、负数、还是零。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.Scanner;

public class IfOrSwitchDemo {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);

System.out.print("请输入整数:");
int num = input.nextInt();

if (num > 0) {
System.out.println(num + "是正整数");
} else if (num < 0) {
System.out.println(num + "是负整数");
} else {
System.out.println(num + "是零");
}

input.close();
}
}

2.2.5 练习

练习1:从键盘输入星期的整数值,输出星期的英文单词

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import java.util.Scanner;

public class SwitchCaseExer1 {
public static void main(String[] args) {
//定义指定的星期
Scanner input = new Scanner(System.in);
System.out.print("请输入星期值:");
int weekday = input.nextInt();

//switch语句实现选择
switch(weekday) {
case 1:
System.out.println("Monday");
break;
case 2:
System.out.println("Tuesday");
break;
case 3:
System.out.println("Wednesday");
break;
case 4:
System.out.println("Thursday");
break;
case 5:
System.out.println("Friday");
break;
case 6:
System.out.println("Saturday");
break;
case 7:
System.out.println("Sunday");
break;
default:
System.out.println("你输入的星期值有误!");
break;
}

input.close();
}
}

练习2:

1
使用 switch 把小写类型的 char型转为大写。只转换 a, b, c, d, e. 其它的输出 “other”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class SwitchCaseExer2 {

public static void main(String[] args) {

char word = 'c';
switch (word) {
case 'a':
System.out.println("A");
break;
case 'b':
System.out.println("B");
break;
case 'c':
System.out.println("C");
break;
case 'd':
System.out.println("D");
break;
case 'e':
System.out.println("E");
break;
default :
System.out.println("other");
}
}
}

练习3:

1
2
3
4
5
6
7
8
9
编写程序:从键盘上读入一个学生成绩,存放在变量score中,根据score的值输出其对应的成绩等级:

score>=90 等级: A
70<=score<90 等级: B
60<=score<70 等级: C
score<60 等级: D

方式一:使用if-else
方式二:使用switch-case: score / 10: 0 - 10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class SwitchCaseExer3 {

public static void main(String[] args) {

Scanner scan = new Scanner(System.in);
System.out.println("请输入学生成绩:");
int score = scan.nextInt();

char grade;//记录学生等级
//方式1:
// if(score >= 90){
// grade = 'A';
// }else if(score >= 70 && score < 90){
// grade = 'B';
// }else if(score >= 60 && score < 70){
// grade = 'C';
// }else{
// grade = 'D';
// }

//方式2:
switch(score / 10){
case 10:
case 9:
grade = 'A';
break;
case 8:
case 7:
grade = 'B';
break;
case 6:
grade = 'C';
break;
default :
grade = 'D';
}

System.out.println("学生成绩为" + score + ",对应的等级为" + grade);

scan.close();
}
}

练习4:

1
2
3
编写一个程序,为一个给定的年份找出其对应的中国生肖。中国的生肖基于12年一个周期,每年用一个动物代表:rat、ox、tiger、rabbit、dragon、snake、horse、sheep、monkey、rooster、dog、pig。

提示:2022年:虎 2022 % 12 == 6

image-20220314005350344

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* @author 尚硅谷-宋红康
* @create 18:55
*/
public class SwitchCaseExer4 {
public static void main(String[] args){
//从键盘输入一个年份
Scanner input = new Scanner(System.in);
System.out.print("请输入年份:");
int year = input.nextInt();
input.close();

//判断
switch(year % 12){
case 0:
System.out.println(year + "是猴年");
break;
case 1:
System.out.println(year + "是鸡年");
break;
case 2:
System.out.println(year + "是狗年");
break;
case 3:
System.out.println(year + "是猪年");
break;
case 4:
System.out.println(year + "是鼠年");
break;
case 5:
System.out.println(year + "是牛年");
break;
case 6:
System.out.println(year + "是虎年");
break;
case 7:
System.out.println(year + "是兔年");
break;
case 8:
System.out.println(year + "是龙年");
break;
case 9:
System.out.println(year + "是蛇年");
break;
case 10:
System.out.println(year + "是马年");
break;
case 11:
System.out.println(year + "是羊年");
break;
default:
System.out.println(year + "输入错误");
}
}
}

练习5:押宝游戏

1
2
3
4
随机产生3个1-6的整数,如果三个数相等,那么称为“豹子”,如果三个数之和大于9,称为“大”,如果三个数之和小于等于9,称为“小”,用户从键盘输入押的是“豹子”、“大”、“小”,并判断是否猜对了

提示:随机数 Math.random()产生 [0,1)范围内的小数
如何获取[a,b]范围内的随机整数呢?(int)(Math.random() * (b - a + 1)) + a

1659112038716

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.util.Scanner;

public class SwitchCaseExer5 {
public static void main(String[] args) {
//1、随机产生3个1-6的整数
int a = (int)(Math.random()*6 + 1);
int b = (int)(Math.random()*6 + 1);
int c = (int)(Math.random()*6 + 1);

//2、押宝
Scanner input = new Scanner(System.in);
System.out.print("请押宝(豹子、大、小):");
String ya = input.next();
input.close();

//3、判断结果
boolean result = false;
//switch支持String类型
switch (ya){
case "豹子": result = a == b && b == c; break;
case "大": result = a + b + c > 9; break;
case "小": result = a + b + c <= 9; break;
default:System.out.println("输入有误!");
}

System.out.println("a,b,c分别是:" + a +"," + b +"," + c );
System.out.println(result ? "猜中了" : "猜错了");
}
}

练习6:

1
2
3
4
5
6
7
8
9
10
11
12
使用switch语句改写下列if语句:

int a = 3;
int x = 100;
if(a==1)
x+=5;
else if(a==2)
x+=10;
else if(a==3)
x+=16;
else
x+=34;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int a = 3;
int x = 100;

switch(a){
case 1:
x += 5;
break;
case 2:
x += 10;
break;
case 3:
x += 16;
break;
default :
x += 34;

}

3. 循环语句

  • 理解:循环语句具有在某些条件满足的情况下,反复执行特定代码的功能。

  • 循环结构分类:

    • for 循环
    • while 循环
    • do-while 循环
  • 循环结构四要素

    • 初始化部分
    • 循环条件部分
    • 循环体部分
    • 迭代部分

3.1 for循环

3.1.1 基本语法

语法格式:

1
2
3
for (①初始化部分; ②循环条件部分; ④迭代部分){
③循环体部分;

执行过程:①-[②-③-④]-[②-③-④]-[②-③-④]-…..-直到②不满足

图示:

image-20220315013023236

说明:

  • for(;;)中的两个;不能多也不能少
  • ①初始化部分可以声明多个变量,但必须是同一个类型,用逗号分隔 –例如: int i=10,j=12,k=13;
  • ②循环条件部分为boolean类型表达式,当值为false时,退出循环
  • ④可以有多个变量更新,用逗号分隔 –例如: ;i++,j++,k++;

3.1.2 应用举例

案例1:使用for循环重复执行某些语句

题目:输出5行HelloWorld

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ForTest1 {
public static void main(String[] args) {
//需求1:控制台输出5行Hello World!
//写法1:
//System.out.println("Hello World!");
//System.out.println("Hello World!");
//System.out.println("Hello World!");
//System.out.println("Hello World!");
//System.out.println("Hello World!");

//写法2:
for(int i = 1;i <= 5;i++){
System.out.println("Hello World!");
}
}
}

案例2:格式的多样性

题目:写出输出的结果

1
2
3
4
5
6
7
8
9
public class ForTest2 {
public static void main(String[] args) {
int num = 1;
for(System.out.print("a");num < 3;System.out.print("c"),num++){
System.out.print("b");

}
}
}

案例3:累加的思想

题目:遍历1-100以内的偶数,并获取偶数的个数,获取所有的偶数的和

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ForTest3 {
public static void main(String[] args) {
int count = 0;//记录偶数的个数
int sum = 0;//记录偶数的和

for(int i = 1;i <= 100;i++){

if(i % 2 == 0){
System.out.println(i);
count++;
sum += i;
}

//System.out.println("偶数的个数为:" + count);
}

System.out.println("偶数的个数为:" + count);
System.out.println("偶数的总和为:" + sum);
}
}

案例4:结合分支结构使用

题目:输出所有的水仙花数,所谓水仙花数是指一个3位数,其各个位上数字立方和等于其本身。例如: 153 = 1*1*1 + 3*3*3 + 5*5*5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ForTest4 {
public static void main(String[] args) {
int sum=0;
for(int i=100;i<1000;i++){
//取出每一位
int ge=i%10;
int shi=i/10%10;
int bai=i/100;
//System.out.println("此轮的每位数字分别是:"+bai+" "+shi+" "+ge);
if((ge*ge*ge+shi*shi*shi+bai*bai*bai)==i){
System.out.println(i);
sum++;
}
}
System.out.println(sum);
}
}

拓展:

1
打印出四位数字中“个位+百位”等于“十位+千位”并且个位数为偶数,千位数为奇数的数字,并打印符合条件的数字的个数。

案例5:结合break的使用

说明:输入两个正整数m和n,求其最大公约数和最小公倍数。

比如:12和20的最大公约数是4,最小公倍数是60。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class ForTest5 {
public static void main(String[] args) {
//输入两个正整数m和n,求其最大公约数和最小公倍数
//公约数: 1 2 3 4 6 / 1 2 4 5 10 --最大公约数是4
//公倍数: 12 24 36 48 60 / 20 40 60 80 --最小公倍数是60
int m=12,n=20;
int maxgy=0; // 最大公约数
int mingb=0; // 最小公倍数
int maxgy2=0;

//1.考虑找最大公约数的话就要看min 如果min的到头了那就最大了
int min=n>m?m:n;
for(int i=min;i>=0;i--){
if(m%i==0&&n%i==0){ //只要同时能被整除那就是公约数
maxgy=i;
break; //第一个满足的就是最大的
}
}
//1.2.笨办法就是从1开始到min范围内找
for(int i=1;i<=min;i++){
if(m%i==0&&n%i==0){ //只要同时能被整除那就是公约数
maxgy2=i;
}
}

//2.考虑找最小公倍数的话就要看max
int max=n>m?n:m;
for(int i=max;i<=m*n;i++){ //从max-m*n范围内 最差就是m*n
if(i%m==0&&i%n==0){ //i同时能整除m和n就是公倍数
mingb=i;
break; //找到第一个就是最小的
}
}
System.out.println("最大公约数1方法:"+maxgy);
System.out.println("最大公约数2方法:"+maxgy2);
System.out.println("最小公倍数:"+mingb);
}
}

说明:

1、我们可以在循环中使用break。一旦执行break,就跳出当前循环结构。

2、小结:如何结束一个循环结构?

​ 结束情况1:循环结构中的循环条件部分返回false

​ 结束情况2:循环结构中执行了break。

3、如果一个循环结构不能结束,那就是一个死循环!我们开发中要避免出现死循环。

3.1.3 练习

练习1:打印1~100之间所有奇数的和

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ForExer1 {

public static void main(String[] args) {

int sum = 0;//记录奇数的和
for (int i = 1; i < 100; i++) {
if(i % 2 != 0){ //不能被2整除就是奇数
sum += i;
}
}
System.out.println("奇数总和为:" + sum);
}
}

练习2:打印1~100之间所有是7的倍数的整数的个数及总和(体会设置计数器的思想)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ForExer2 {

public static void main(String[] args) {

int sum = 0;//记录总和
int count = 0;//记录个数
for (int i = 1; i < 100; i++) {
if(i % 7 == 0){
sum += i;
count++;
}
}
System.out.println("1~100之间所有是7的倍数的整数的和为:" + sum);
System.out.println("1~100之间所有是7的倍数的整数的个数为:" + count);
}
}

练习3:

编写程序从1循环到150,并在每行打印一个值,另外在每个3的倍数行上打印出“foo”,在每个5的倍数行上打印“biz”,在每个7的倍数行上打印输出“baz”。

image-20220315013722962
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ForExer3 {
public static void main(String[] args) {
for (int i = 1; i < =150; i++) {
if(i%3==0){
System.out.println(i+" foo");
}else if(i%5==0){
System.out.println(i+" biz");
}else if(i%7==0){
System.out.println(i+" baz");
}else{
System.out.println(i);
}
}
}
}

3.2 while循环

3.2.1 基本语法

语法格式:

1
2
3
4
5
①初始化部分
while(②循环条件部分){
③循环体部分;
④迭代部分;
}

执行过程:①-(②-③-④)-(②-③-④)-(②-③-④)-…-直到②不符合结束

图示:

image-20220315013023236

说明:

  • while(循环条件)中循环条件必须是boolean类型。
  • 注意不要忘记声明④迭代部分。否则,循环将不能结束,变成死循环。
  • for循环和while循环可以相互转换。二者没有性能上的差别。实际开发中,根据具体结构的情况,选择哪个格式更合适、美观。
  • for循环与while循环的区别:初始化条件部分的作用域不同。

3.2.2 应用举例

案例1:输出5行HelloWorld!

1
2
3
4
5
6
7
8
9
10
class WhileTest1 {
public static void main(String[] args) {

int i = 1;
while(i <= 5){
System.out.println("Hello World!");
i++;
}
}
}

案例2:遍历1-100的偶数,并计算所有偶数的和、偶数的个数(累加的思想)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class WhileTest2 {
public static void main(String[] args) {
//遍历1-100的偶数,并计算所有偶数的和、偶数的个数(累加的思想)
int num = 1;

int sum = 0;//记录1-100所有的偶数的和
int count = 0;//记录1-100之间偶数的个数

while(num <= 100){

if(num % 2 == 0){
System.out.println(num);
sum += num;
count++;
}

//迭代条件
num++;
}

System.out.println("偶数的总和为:" + sum);
System.out.println("偶数的个数为:" + count);
}
}

案例3:猜数字游戏

1
2
3
4
5
随机生成一个100以内的数,猜这个随机数是多少?

从键盘输入数,如果大了,提示大了;如果小了,提示小了;如果对了,就不再猜了,并统计一共猜了多少次。

提示:生成一个[a,b] 范围的随机数的方式:(int)(Math.random() * (b - a + 1) + a)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* @author 尚硅谷-宋红康
* @create 16:42
*/
public class GuessNumber {
public static void main(String[] args) {
//获取一个随机数
int random = (int) (Math.random() * 100) + 1;

//记录猜的次数
int count = 1;

//实例化Scanner
Scanner scan = new Scanner(System.in);
System.out.println("请输入一个整数(1-100):");
int guess = scan.nextInt();

while (guess != random) {

if (guess > random) {
System.out.println("猜大了");
} else if (guess < random) {
System.out.println("猜小了");
}

System.out.println("请输入一个整数(1-100):");
guess = scan.nextInt();
//累加猜的次数
count++;

}

System.out.println("猜中了!");
System.out.println("一共猜了" + count + "次");
}
}

案例4:折纸珠穆朗玛峰

1
2
世界最高山峰是珠穆朗玛峰,它的高度是8848.86米,假如我有一张足够大的纸,它的厚度是0.1毫米。
请问,我折叠多少次,可以折成珠穆朗玛峰的高度?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* @author 尚硅谷-宋红康
* @create 19:08
*/
public class ZFTest {
public static void main(String[] args) {
//定义一个计数器,初始值为0
int count = 0;

//定义珠穆朗玛峰的高度
int zf = 8848860;//单位:毫米

double paper = 0.1;//单位:毫米

while(paper < zf){
//在循环中执行累加,对应折叠了多少次
count++;
paper *= 2;//循环的执行过程中每次纸张折叠,纸张的厚度要加倍
}

//打印计数器的值
System.out.println("需要折叠:" + count + "次");
System.out.println("折纸的高度为" + paper/1000 + "米,超过了珠峰的高度");
}
}

3.2.3 练习

练习:从键盘输入整数,输入0结束,统计输入的正数、负数的个数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.Scanner;

public class Test05While {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);

int positive = 0; //记录正数的个数
int negative = 0; //记录负数的个数
int num = 1; //初始化为特殊值,使得第一次循环条件成立
while(num != 0){
System.out.print("请输入整数(0表示结束):");
num = input.nextInt();

if(num > 0){
positive++;
}else if(num < 0){
negative++;
}
}
System.out.println("正数个数:" + positive);
System.out.println("负数个数:" + negative);

input.close();
}
}

3.3 do-while循环

3.3.1 基本语法

语法格式:

1
2
3
4
5
①初始化部分;
do{
③循环体部分
④迭代部分
}while(②循环条件部分);

执行过程:①-③-④-②-③-④-②-③-④-…-②

图示:

image-20220512165558698

说明:

  • 结尾while(循环条件)中循环条件必须是boolean类型
  • do{}while();最后有一个分号
  • do-while结构的循环体语句是至少会执行一次,这个和for和while是不一样的
  • 循环的三个结构for、while、do-while三者是可以相互转换的。

3.3.2 应用举例

案例1:遍历1-100的偶数,并计算所有偶数的和、偶数的个数(累加的思想)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class DoWhileTest1 {
public static void main(String[] args) {

//遍历1-100的偶数,并计算所有偶数的和、偶数的个数(累加的思想)
//初始化部分
int num = 1;

int sum = 0;//记录1-100所有的偶数的和
int count = 0;//记录1-100之间偶数的个数

do{
//循环体部分
if(num % 2 == 0){
System.out.println(num);
sum += num;
count++;
}

num++;//迭代部分


}while(num <= 100); //循环条件部分


System.out.println("偶数的总和为:" + sum);
System.out.println("偶数的个数为:" + count);
}
}

案例2:体会do-while至少会执行一次循环体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class DoWhileTest2 {
public static void main(String[] args) {
//while循环:
int num1 = 10;
while(num1 > 10){
System.out.println("hello:while");
num1--;
}

//do-while循环:
int num2 = 10;
do{
System.out.println("hello:do-while");
num2--;
}while(num2 > 10);

}
}

案例3:ATM取款

1
2
3
4
5
6
7
8
声明变量balance并初始化为0,用以表示银行账户的余额,下面通过ATM机程序实现存款,取款等功能。

=========ATM========
1、存款
2、取款
3、显示余额
4、退出
请选择(1-4):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import java.util.Scanner;

/**
* @author 尚硅谷-宋红康
* @create 19:12
*/
public class ATM {
public static void main(String[] args) {

//初始化条件
double balance = 0.0;//表示银行账户的余额
Scanner scan = new Scanner(System.in);
boolean isFlag = true;//用于控制循环的结束

do{
System.out.println("=========ATM========");
System.out.println("\t1、存款");
System.out.println("\t2、取款");
System.out.println("\t3、显示余额");
System.out.println("\t4、退出");
System.out.print("请选择(1-4):");

int selection = scan.nextInt();

switch(selection){
case 1:
System.out.print("要存款的额度为:");
double addMoney = scan.nextDouble();
if(addMoney > 0){
balance += addMoney;
}
break;
case 2:
System.out.print("要取款的额度为:");
double minusMoney = scan.nextDouble();
if(minusMoney > 0 && balance >= minusMoney){
balance -= minusMoney;
}else{
System.out.println("您输入的数据非法或余额不足");
}
break;
case 3:
System.out.println("当前的余额为:" + balance);
break;
case 4:
System.out.println("欢迎下次进入此系统。^_^");
isFlag = false;
break;
default:
System.out.println("请重新选择!");
break;
}

}while(isFlag);

//资源关闭
scan.close();

}
}

3.3.3 练习

练习1:随机生成一个100以内的数,猜这个随机数是多少?

从键盘输入数,如果大了提示,大了;如果小了,提示小了;如果对了,就不再猜了,并统计一共猜了多少次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.util.Scanner;

public class DoWhileExer {
public static void main(String[] args) {
//随机生成一个100以内的整数
/*
Math.random() ==> [0,1)的小数
Math.random()* 100 ==> [0,100)的小数
(int)(Math.random()* 100) ==> [0,100)的整数
*/
int num = (int)(Math.random()* 100);
//System.out.println(num);

//声明一个变量,用来存储猜的次数
int count = 0;

Scanner input = new Scanner(System.in);
int guess;//提升作用域
do{
System.out.print("请输入100以内的整数:");
guess = input.nextInt();

//输入一次,就表示猜了一次
count++;

if(guess > num){
System.out.println("大了");
}else if(guess < num){
System.out.println("小了");
}
}while(num != guess);

System.out.println("一共猜了:" + count+"次");

input.close();
}
}

3.4 对比三种循环结构

  • 三种循环结构都具有四个要素:
    • 循环变量的初始化条件
    • 循环条件
    • 循环体语句块
    • 循环变量的修改的迭代表达式
  • 从循环次数角度分析
    • do-while循环至少执行一次循环体语句。
    • for和while循环先判断循环条件语句是否成立,然后决定是否执行循环体。
  • 如何选择
    • 遍历有明显的循环次数(范围)的需求,选择for循环
    • 遍历没有明显的循环次数(范围)的需求,选择while循环
    • 如果循环体语句块至少执行一次,可以考虑使用do-while循环
    • 本质上:三种循环之间完全可以互相转换,都能实现循环的功能

3.5 “无限”循环

类似:死循环

3.5.1 基本语法

语法格式:

  • 最简单”无限”循环格式:while(true) , for(;;)

适用场景:

  • 开发中,有时并不确定需要循环多少次,需要根据循环体内部某些条件,来控制循环的结束(使用break)。
  • 如果此循环结构不能终止,则构成了死循环!开发中要避免出现死循环。

3.5.2 应用举例

案例1:实现爱你到永远…

1
2
3
4
5
6
7
8
public class EndlessFor1 {
public static void main(String[] args) {
for (;;){
System.out.println("我爱你!");
}
// System.out.println("end");//永远无法到达的语句,编译报错
}
}
1
2
3
4
5
6
7
public class EndlessFor2 {
public static void main(String[] args) {
for (; true;){ //条件永远成立,死循环
System.out.println("我爱你!");
}
}
}
1
2
3
4
5
6
7
public class EndlessFor3 {
public static void main(String[] args) {
for (int i=1; i<=10; ){ //循环变量没有修改,条件永远成立,死循环
System.out.println("我爱你!");
}
}
}

思考:如下代码执行效果

1
2
3
4
5
6
7
public class EndlessFor4 {
public static void main(String[] args) {
for (int i=1; i>=10; ){ //一次都不执行
System.out.println("我爱你!");
}
}
}

案例2:从键盘读入个数不确定的整数,并判断读入的正数和负数的个数,输入为0时结束程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.util.Scanner;

class PositiveNegative {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);

int positiveNumber = 0;//统计正数的个数
int negativeNumber = 0;//统计负数的个数
for(;;){ //while(true){
System.out.println("请输入一个整数:(输入为0时结束程序)");
int num = scanner.nextInt();
if(num > 0){
positiveNumber++;
}else if(num < 0){
negativeNumber++;
}else{
System.out.println("程序结束");
break;
}
}
System.out.println("正数的个数为:"+ positiveNumber);
System.out.println("负数的个数为:"+ negativeNumber);

scanner.close();
}
}

3.6 嵌套循环(或多重循环)

3.6.1 使用说明

  • 所谓嵌套循环,是指一个循环结构A的循环体是另一个循环结构B。比如,for循环里面还有一个for循环,就是嵌套循环。其中,for ,while ,do-while均可以作为外层循环或内层循环。
    • 外层循环:循环结构A
    • 内层循环:循环结构B
  • 实质上,嵌套循环就是把内层循环当成外层循环的循环体。只有当内层循环的循环条件为false时,才会完全跳出内层循环,才可结束外层的当次循环,开始下一次的外层循环。
  • 设外层循环次数为m次,内层为n次,则内层循环体实际上需要执行m*n次。
  • 技巧:从二维图形的角度看,外层循环控制行数,内层循环控制列数
  • 开发经验:实际开发中,我们最多见到的嵌套循环是两层。一般不会出现超过三层的嵌套循环。如果将要出现,一定要停下来重新梳理业务逻辑,重新思考算法的实现,控制在三层以内。否则,可读性会很差。

例如:两个for嵌套循环格式

1
2
3
4
5
6
7
for(初始化语句①; 循环条件语句②; 迭代语句⑦) {
for(初始化语句③; 循环条件语句④; 迭代语句⑥) {
循环体语句⑤;
}
}

//执行过程:① - ② - ③ - ④ - ⑤ - ⑥ - ④ - ⑤ - ⑥ - ... - ④ - ⑦ - ② - ③ - ④ - ⑤ - ⑥ - ④..

执行特点:外层循环执行一次,内层循环执行一轮。

3.6.2 应用举例

案例1:打印5行6个*

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class ForForTest1 {
public static void main(String[] args) {
/*

******
******
******
******
******

*/

for(int j = 1;j <= 5;j++){

for(int i = 1;i <= 6;i++){
System.out.print("*");
}

System.out.println();
}
}
}

案例2:打印5行直角三角形

1
2
3
4
5
*
**
***
****
*****
1
2
3
4
5
6
7
8
9
10
public class ForForTest2 {
public static void main(String[] args){
for (int i = 1; i <= 5; i++) {
for (int j = 1; j <= i; j++) {
System.out.print("*");
}
System.out.println();
}
}
}

案例3:打印5行倒直角三角形

1
2
3
4
5
*****
****
***
**
*
1
2
3
4
5
6
7
8
9
10
11
12
public class ForForTest3 {
public static void main(String[] args){
for(int i = 1;i <= 5;i++){
for(int j = 1;j <= 6 - i;j++){
System.out.print("*");

}
System.out.println();

}
}
}

案例4:打印”菱形”形状的图案

1
2
3
4
5
6
7
8
9
        * 
* * *
* * * * *
* * * * * * *
* * * * * * * * *
* * * * * * *
* * * * *
* * *
*
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class ForForTest4 {

public static void main(String[] args) {
/*
上半部分 i m(表示-的个数) n(表示*的个数)关系式:2*i + m = 10 --> m = 10 - 2*i
--------* 1 8 1 n = 2 * i - 1
------* * * 2 6 3
----* * * * * 3 4 5
--* * * * * * * 4 2 7
* * * * * * * * * 5 0 9

下半部分 i m n 关系式: m = 2 * i
--* * * * * * * 1 2 7 n = 9 - 2 * i
----* * * * * 2 4 5
------* * * 3 6 3
--------* 4 8 1

*/
//上半部分
for (int i = 1; i <= 5; i++) {
//-
for (int j = 1; j <= 10 - 2 * i; j++) {
System.out.print(" ");
}
//*
for (int k = 1; k <= 2 * i - 1; k++) {
System.out.print("* ");
}
System.out.println();
}
//下半部分
for (int i = 1; i <= 4; i++) {
//-
for (int j = 1; j <= 2 * i; j++) {
System.out.print(" ");
}

//*
for (int k = 1; k <= 9 - 2 * i; k++) {
System.out.print("* ");
}
System.out.println();
}
}

}

案例5:九九乘法表

image-20221113193013204

1
2
3
4
5
6
7
8
9
10
public class ForForTest5 {
public static void main(String[] args) {
for (int i = 1; i <= 9; i++) {
for (int j = 1; j <= i; j++) {
System.out.print(i + "*" + j + "=" + (i * j) + "\t");
}
System.out.println();
}
}
}

3.6.3 练习

练习1:将一天中的时间打印到控制台

1
2
3
4
5
6
7
8
9
public class ForForDemo {
public static void main (String[] args) {
for (int hour = 0;hour < 24 ;hour++ ) {
for (int min = 0; min < 60 ; min++) {
System.out.println(hour + "时" + min +"分");
}
}
}
}

4. 关键字break和continue的使用

4.1 break和continue的说明

1
2
3
4
5
6
			  适用范围			      在循环结构中使用的作用						相同点

break switch-case
循环结构 一旦执行,就结束(或跳出)当前循环结构 此关键字的后面,不能声明语句

continue 循环结构 一旦执行,就结束(或跳出)当次循环结构 此关键字的后面,不能声明语句

此外,很多语言都有goto语句,goto语句可以随意将控制转移到程序中的任意一条语句上,然后执行它,但使程序容易出错。Java中的break和continue是不同于goto的。

4.2 应用举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class BreakContinueTest1 {
public static void main(String[] args) {

for(int i = 1;i <= 10;i++){

if(i % 4 == 0){
//break;//123
continue;//123567910
//如下的语句不可能被执行,编译不通过
//System.out.println("今晚迪丽热巴要约我吃饭");
}

System.out.print(i);
}

System.out.println("####");

//嵌套循环中的使用
for(int i = 1;i <= 4;i++){

for(int j = 1;j <= 10;j++){
if(j % 4 == 0){
//break; //结束的是包裹break关键字的最近的一层循环!
continue;//结束的是包裹break关键字的最近的一层循环的当次!
}
System.out.print(j);
}
System.out.println();
}

}
}

4.3 带标签的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
break语句用于终止某个语句块的执行
{ ……
break;
……
}

break语句出现在多层嵌套的语句块中时,可以通过标签指明要终止的是哪一层语句块
label1: { ……
label2: { ……
label3: { ……
break label2;
……
}
}
}
  • continue语句出现在多层嵌套的循环语句体中时,也可以通过标签指明要跳过的是哪一层循环。

  • 标号语句必须紧接在循环的头部。标号语句不能用在非循环语句的前面。

  • 举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class BreakContinueTest2 {
public static void main(String[] args) {
l:for(int i = 1;i <= 4;i++){

for(int j = 1;j <= 10;j++){
if(j % 4 == 0){
//break l;
continue l;
}
System.out.print(j);
}
System.out.println();
}
}
}

4.4 经典案例

题目:找出100以内所有的素数(质数)?100000以内的呢?

目的:不同的代码的实现方式,可以效率差别很大。

分析:素数(质数):只能被1和它本身整除的自然数。 —> 从2开始,到这个数-1为止,此范围内没有这个数的约数。则此数是一个质数。
比如:2、3、5、7、11、13、17、19、23、…

实现方式1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class PrimeNumberTest {
public static void main(String[] args) {


//boolean isFlag = true; //用于标识i是否被除尽过

long start = System.currentTimeMillis(); //记录当前时间距离1970-1-1 00:00:00的毫秒数

int count = 0;//记录质数的个数


for(int i = 2;i <= 100000;i++){ //i

boolean isFlag = true; //用于标识i是否被除尽过

for(int j = 2;j <= i - 1;j++){

if(i % j == 0){ //表明i有约数
isFlag = false;
}

}

//判断i是否是质数
if(isFlag){ //如果isFlag变量没有给修改过值,就意味着i没有被j除尽过。则i是一个质数
//System.out.println(i);
count++;
}

//重置isFlag
//isFlag = true;

}

long end = System.currentTimeMillis();
System.out.println("质数的个数为:" + count);
System.out.println("执行此程序花费的毫秒数为:" + (end - start)); //16628

}
}

实现方式2:针对实现方式1进行优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class PrimeNumberTest1 {
public static void main(String[] args) {

long start = System.currentTimeMillis(); //记录当前时间距离1970-1-1 00:00:00的毫秒数

int count = 0;//记录质数的个数

for(int i = 2;i <= 100000;i++){ //i

boolean isFlag = true; //用于标识i是否被除尽过

for(int j = 2;j <= Math.sqrt(i);j++){ //优化2:将循环条件中的i改为Math.sqrt(i)

if(i % j == 0){ //表明i有约数
isFlag = false;
break;//优化1:主要针对非质数起作用
}

}

//判断i是否是质数
if(isFlag){ //如果isFlag变量没有给修改过值,就意味着i没有被j除尽过。则i是一个质数
//System.out.println(i);
count++;
}

}

long end = System.currentTimeMillis();
System.out.println("质数的个数为:" + count);
System.out.println("执行此程序花费的毫秒数为:" + (end - start));//1062

}
}

实现方式3(选做):使用continue + 标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class PrimeNumberTest2 {
public static void main(String[] args) {

long start = System.currentTimeMillis(); //记录当前时间距离1970-1-1 00:00:00的毫秒数

int count = 0;//记录质数的个数

label:for(int i = 2;i <= 100000;i++){ //i

for(int j = 2;j <= Math.sqrt(i);j++){ //优化2:将循环条件中的i改为Math.sqrt(i)

if(i % j == 0){ //表明i有约数
continue label;
}

}
//一旦程序能执行到此位置,说明i就是一个质数
System.out.println(i);
count++;
}


long end = System.currentTimeMillis();
System.out.println("质数的个数为:" + count);
System.out.println("执行此程序花费的毫秒数为:" + (end - start));//1062

}
}

4.5 练习

练习1:

1
2
3
生成 1-100 之间的随机数,直到生成了 97 这个数,看看一共用了几次?

提示:使用 (int)(Math.random() * 100) + 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class NumberGuessTest {
public static void main(String[] args) {
int count = 0;//记录循环的次数(或生成随机数进行比较的次数)
while(true){
int random = (int)(Math.random() * 100) + 1;
count++;
if(random == 97){
break;
}
}

System.out.println("直到生成随机数97,一共比较了" + count + "次");

}
}

5. Scanner:键盘输入功能的实现

  • 如何从键盘获取不同类型(基本数据类型、String类型)的变量:使用Scanner类。

  • 键盘输入代码的四个步骤:

    1. 导包:import java.util.Scanner;
    2. 创建Scanner类型的对象:Scanner scan = new Scanner(System.in);
    3. 调用Scanner类的相关方法(next() / nextXxx()),来获取指定类型的变量
    4. 释放资源:scan.close();
  • 注意:需要根据相应的方法,来输入指定类型的值。如果输入的数据类型与要求的类型不匹配时,会报异常导致程序终止。

5.1 各种类型的数据输入

案例:小明注册某交友网站,要求录入个人相关信息。如下:

请输入你的网名、你的年龄、你的体重、你是否单身、你的性别等情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//① 导包
import java.util.Scanner;

public class ScannerTest1 {

public static void main(String[] args) {
//② 创建Scanner的对象
//Scanner是一个引用数据类型,它的全名称是java.util.Scanner
//scanner就是一个引用数据类型的变量了,赋给它的值是一个对象(对象的概念我们后面学习,暂时先这么叫)
//new Scanner(System.in)是一个new表达式,该表达式的结果是一个对象
//引用数据类型 变量 = 对象;
//这个等式的意思可以理解为用一个引用数据类型的变量代表一个对象,所以这个变量的名称又称为对象名
//我们也把scanner变量叫做scanner对象
Scanner scanner = new Scanner(System.in);//System.in默认代表键盘输入

Scanner input=new Scanner(System.in);
//网名 string
String name=input.next();
//年龄 int
int age=input.nextInt();
//体重 double
double weight=input.nextDouble();
//单身 string
String ismarried=input.next();
//性别 string
String sex=input.next();
System.out.println("网名:" + name + "\n年龄:" + age + "\n体重:" + weight +"\n单身:" + ismarried + "\n性别:" + sex);

//④ 关闭资源
scanner.close();
}
}

5.2 练习

练习1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
大家都知道,男大当婚,女大当嫁。那么女方家长要嫁女儿,当然要提出一定的条件:高:180cm以上;富:财富1千万以上;帅:是。

如果这三个条件同时满足,则:“我一定要嫁给他!!!”
如果三个条件有为真的情况,则:“嫁吧,比上不足,比下有余。”
如果三个条件都不满足,则:“不嫁!”

提示:
System.out.println(“身高: (cm));
scanner.nextInt();

System.out.println(“财富: (千万));
scanner.nextDouble();

System.out.println(“帅否: (true/false));
scanner.nextBoolean();



System.out.println(“帅否: (是/否));
scanner.next(); "是".equals(str)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import java.util.Scanner;

class ScannerExer1 {
public static void main(String[] args) {

Scanner scan = new Scanner(System.in);

System.out.println("请输入你的身高:(cm)");
int height = scan.nextInt();

System.out.println("请输入你的财富:(以千万为单位)");
double wealth = scan.nextDouble();

/*

方式1:关于是否帅问题,我们使用boolean类型接收

System.out.println("帅否?(true/false)");
boolean isHandsome = scan.nextBoolean();

//判断
if(height >= 180 && wealth >= 1.0 && isHandsome){ //不建议isHandsome == true
System.out.println("我一定要嫁给他!!!");
}else if(height >= 180 || wealth >= 1.0 || isHandsome){
System.out.println("嫁吧,比上不足,比下有余。");
}else{
System.out.println("不嫁");
}

*/

//方式2:关于是否帅问题,我们使用String类型接收
System.out.println("帅否?(是/否)");
String isHandsome = scan.next();

//判断
if(height >= 180 && wealth >= 1.0 && isHandsome == "是"){ //知识点:判断两个字符串是否相等,使用String的equals()
System.out.println("我一定要嫁给他!!!");
}else if(height >= 180 || wealth >= 1.0 || isHandsome == "是"){
System.out.println("嫁吧,比上不足,比下有余。");
}else{
System.out.println("不嫁");
}

//关闭资源
scan.close();
}
}

练习2:

1
2
3
我家的狗5岁了,5岁的狗相当于人类多大呢?其实,狗的前两年每一年相当于人类的10.5岁,之后每增加一年就增加四岁。那么5岁的狗相当于人类多少年龄呢?应该是:10.5 + 10.5 + 4 + 4 + 4 = 33岁。

编写一个程序,获取用户输入的狗的年龄,通过程序显示其相当于人类的年龄。如果用户输入负数,请显示一个提示信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.Scanner;

class ScannerExer2 {
public static void main(String[] args) {

Scanner scan = new Scanner(System.in);

System.out.println("请输入狗狗的年龄:");
int dogAge = scan.nextInt();

//通过分支语句,判断狗狗相当于人的年龄
if(dogAge < 0){
System.out.println("你输入的狗狗的年龄不合法");
}else if(dogAge <= 2){
System.out.println("相当于人的年龄:" + (dogAge * 10.5));
}else{
System.out.println("相当于人的年龄:" + (2 * 10.5 + (dogAge - 2) * 4));
}

//关闭资源
scan.close();

}
}

6. 如何获取一个随机数(Math.random())

如何产生一个指定范围的随机整数?

1、Math类的random()的调用,会返回一个[0,1)范围的一个double型值

2、Math.random() * 100 —> [0,100)
(int)(Math.random() * 100) —> [0,99]
(int)(Math.random() * 100) + 5 —-> [5,104]

3、如何获取[a,b]范围内的随机整数呢?(int)(Math.random() * (b - a + 1)) + a

4、举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MathRandomTest {
public static void main(String[] args) {
double value1=Math.random(); // [0,1)随机一个double值
System.out.println(value1);

double value100d=Math.random()*100; // [0,100)随机一个double值
System.out.println(value100d);

int value100i=(int)(Math.random()*100); // [0,100)随机一个int值
System.out.println(value100i);

int a=4;
int b=10;
int valueab=(int)(Math.random()*(b-a+1))+a; // [a,b]范围内的随机整数
System.out.println(valueab);
}
}

第05章_数组


本章专题与脉络

第1阶段:Java基本语法-第05章

1. 数组的概述

1.1 为什么需要数组

需求分析1:

需要统计某公司50个员工的工资情况,例如计算平均工资、找到最高工资等。用之前知识,首先需要声明50个变量来分别记录每位员工的工资,这样会很麻烦。因此我们可以将所有的数据全部存储到一个容器中统一管理,并使用容器进行计算。

需求分析2:

snipaste_20220317_000101

容器的概念:

  • 生活中的容器:水杯(装水等液体),衣柜(装衣服等物品),集装箱(装货物等)。
  • 程序中的容器:将多个数据存储到一起,每个数据称为该容器的元素。

1.2 数组的概念

  • 数组(Array),是多个相同类型数据按一定顺序排列的集合,并使用一个名字命名,并通过编号的方式对这些数据进行统一管理。

  • 数组中的概念

    • 数组名
    • 下标(或索引)
    • 元素
    • 数组的长度

image-20220317000952499

数组的特点:

  • 数组本身是引用数据类型,而数组中的元素可以是任何数据类型,包括基本数据类型和引用数据类型。
  • 创建数组对象会在内存中开辟一整块连续的空间。占据的空间的大小,取决于数组的长度和数组中元素的类型。
  • 数组中的元素在内存中是依次紧密排列的,有序的。
  • 数组,一旦初始化完成,其长度就是确定的。数组的长度一旦确定,就不能修改
  • 我们可以直接通过下标(或索引)的方式调用指定位置的元素,速度很快。
  • 数组名中引用的是这块连续空间的首地址。

1.3 数组的分类

1、按照元素类型分:

  • 基本数据类型元素的数组:每个元素位置存储基本数据类型的值
  • 引用数据类型元素的数组:每个元素位置存储对象(本质是存储对象的首地址)(在面向对象部分讲解)

2、按照维度分:

  • 一维数组:存储一组数据
  • 二维数组:存储多组数据,相当于二维表,一行代表一组数据,只是这里的二维表每一行长度不要求一样。

image-20211221164709624

2. 一维数组的使用

2.1 一维数组的声明

格式:

1
2
3
4
5
//推荐
元素的数据类型[] 一维数组的名称;

//不推荐
元素的数据类型 一维数组名[];

举例:

1
2
3
4
int[] arr;
int arr1[];
double[] arr2;
String[] arr3; //引用类型变量数组

数组的声明,需要明确:

(1)数组的维度:在Java中数组的符号是[],[]表示一维,[][]表示二维。

(2)数组的元素类型:即创建的数组容器可以存储什么数据类型的数据。元素的类型可以是任意的Java的数据类型。例如:int、String、Student等。

(3)数组名:就是代表某个数组的标识符,数组名其实也是变量名,按照变量的命名规范来命名。数组名是个引用数据类型的变量,因为它代表一组数据。

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ArrayTest1 {
public static void main(String[] args) {
//比如,要存储一个小组的成绩
int[] scores;
int grades[];
// System.out.println(scores);//未初始化不能使用

//比如,要存储一组字母
char[] letters;

//比如,要存储一组姓名
String[] names;

//比如,要存储一组价格
double[] prices;

}
}

注意:Java语言中声明数组时不能指定其长度(数组中元素的个数)。 例如: int a[5]; //非法

2.2 一维数组的初始化

2.2.1 静态初始化

  • 如果数组变量的初始化和数组元素的赋值操作同时进行,那就称为静态初始化。

  • 静态初始化,本质是用静态数据(编译时已知)为数组初始化。此时数组的长度=sum(静态数据)。

  • 一维数组声明和静态初始化格式1:

    1
    2
    3
    4
    5
    6
    数据类型[] 数组名 = new 数据类型[]{元素1,元 素2,元素3,...};



    数据类型[] 数组名;
    数组名 = new 数据类型[]{元素1,元素2,元素3,...};
    • new:关键字,创建数组使用的关键字。因为数组本身是引用数据类型,所以要用new创建数组实体。

例如,定义存储1,2,3,4,5整数的数组容器。

1
2
3
4
int[] arr = new int[]{1,2,3,4,5};//正确
//或
int[] arr;
arr = new int[]{1,2,3,4,5};//正确
  • 一维数组声明和静态初始化格式2:
1
数据类型[] 数组名 = {元素1,元素2,元素3...};//必须在一个语句中完成,不能分成两个语句写

例如,定义存储1,2,3,4,5整数的数组容器

1
2
3
4
int[] arr = {1,2,3,4,5};//正确

int[] arr;
arr = {1,2,3,4,5};//错误

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ArrayTest2 {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};//右边不需要写new int[]

int[] nums;
nums = new int[]{10,20,30,40}; //声明和初始化在两个语句完成,就不能使用new int[]

char[] word = {'h','e','l','l','o'};

String[] heros = {"袁隆平","邓稼先","钱学森"};

System.out.println("arr数组:" + arr);//arr数组:[I@1b6d3586
System.out.println("nums数组:" + nums);//nums数组:[I@4554617c
System.out.println("word数组:" + word);//word数组:[C@74a14482
System.out.println("heros数组:" + heros);//heros数组:[Ljava.lang.String;@1540e19d
}
}

2.2.2 动态初始化

数组变量的初始化和数组元素的赋值操作分开进行,即为动态初始化。

动态初始化中,只确定了元素的个数(即数组的长度),而元素值此时只是默认值,还并未真正赋自己期望的值。真正期望的数据需要后续单独一个一个赋值。

格式:

1
2
3
4
5
6
数组存储的元素的数据类型[] 数组名字 = new 数组存储的元素的数据类型[长度];



数组存储的数据类型[] 数组名字;
数组名字 = new 数组存储的数据类型[长度];
  • [长度]:数组的长度,表示数组容器中可以最多存储多少个元素。

  • 注意:数组有定长特性,长度一旦指定,不可更改。和水杯道理相同,买了一个2升的水杯,总容量就是2升是固定的。

举例1:正确写法

1
2
3
4
int[] arr = new int[5];

int[] arr;
arr = new int[5];

举例2:错误写法

1
int[] arr = new int[5]{1,2,3,4,5};//错误的,后面有{}指定元素列表,就不需要在[]中指定元素个数了。

2.3 一维数组的使用

2.3.1 数组的长度

  • 数组的元素总个数,即数组的长度
  • 每个数组都有一个属性length指明它的长度,例如:arr.length 指明数组arr的长度(即元素个数)
  • 每个数组都具有长度,而且一旦初始化,其长度就是确定,且是不可变的。

2.3.2 数组元素的引用

如何表示数组中的一个元素?

每一个存储到数组的元素,都会自动的拥有一个编号,从0开始,这个自动编号称为数组索引(index)或下标,可以通过数组的索引/下标访问到数组中的元素。

1
数组名[索引/下标]

数组的下标范围?

Java中数组的下标从[0]开始,下标范围是[0, 数组的长度-1],即[0, 数组名.length-1]

数组元素下标可以是整型常量或整型表达式。如a[3] , b[i] , c[6*i];

举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ArrayTest3 {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};

System.out.println("arr数组的长度:" + arr.length);
System.out.println("arr数组的第1个元素:" + arr[0]);//下标从0开始
System.out.println("arr数组的第2个元素:" + arr[1]);
System.out.println("arr数组的第3个元素:" + arr[2]);
System.out.println("arr数组的第4个元素:" + arr[3]);
System.out.println("arr数组的第5个元素:" + arr[4]);

//修改第1个元素的值
//此处arr[0]相当于一个int类型的变量
arr[0] = 100;
System.out.println("arr数组的第1个元素:" + arr[0]);
}
}

2.4 一维数组的遍历

将数组中的每个元素分别获取出来,就是遍历。for循环与数组的遍历是绝配。

举例1

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ArrayTest4 {
public static void main(String[] args) {
int[] arr = new int[]{1,2,3,4,5};
//打印数组的属性,输出结果是5
System.out.println("数组的长度:" + arr.length);

//遍历输出数组中的元素
System.out.println("数组的元素有:");
for(int i=0; i<arr.length; i++){
System.out.println(arr[i]);
}
}
}

举例2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class ArrayTest5 {
public static void main(String[] args) {
int[] arr = new int[5];

System.out.println("arr数组的长度:" + arr.length);
System.out.print("存储数据到arr数组之前:[");
for (int i = 0; i < arr.length; i++) {
if(i==0){
System.out.print(arr[i]);
}else{
System.out.print("," + arr[i]);
}
}
System.out.println("]");

//初始化
/*
arr[0] = 2;
arr[1] = 4;
arr[2] = 6;
arr[3] = 8;
arr[4] = 10;
*/

for (int i = 0; i < arr.length; i++) {
arr[i] = (i+1) * 2;
}

System.out.print("存储数据到arr数组之后:[");
for (int i = 0; i < arr.length; i++) {
if(i==0){
System.out.print(arr[i]);
}else{
System.out.print("," + arr[i]);
}
}
System.out.println("]");
}
}

2.5 数组元素的默认值

数组是引用类型,当我们使用动态初始化方式创建数组时,元素值只是默认值。例如:

1
2
3
4
5
6
public class ArrayTest6 {
public static void main(String argv[]){
int a[]= new int[5];
System.out.println(a[3]); //a[3]的默认值为0
}
}

对于基本数据类型而言,默认初始化值各有不同。

对于引用数据类型而言,默认初始化值为null(注意与0不同!)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class ArrayTest7 {
public static void main(String[] args) {
//存储26个字母
char[] letters = new char[26];
System.out.println("letters数组的长度:" + letters.length);
System.out.print("存储字母到letters数组之前:[");
for (int i = 0; i < letters.length; i++) {
if(i==0){
System.out.print(letters[i]);
}else{
System.out.print("," + letters[i]);
}
}
System.out.println("]");

//存储5个姓名
String[] names = new String[5];
System.out.println("names数组的长度:" + names.length);
System.out.print("存储姓名到names数组之前:[");
for (int i = 0; i < names.length; i++) {
if(i==0){
System.out.print(names[i]);
}else{
System.out.print("," + names[i]);
}
}
System.out.println("]");
}
}

3. 一维数组内存分析

3.1 Java虚拟机的内存划分

为了提高运算效率,就对空间进行了不同区域的划分,因为每一片区域都有特定的处理数据方式和内存管理方式。

JVM架构-简图

区域名称 作用
虚拟机栈 用于存储正在执行的每个Java方法的局部变量表等。局部变量表存放了编译期可知长度
的各种基本数据类型、对象引用,方法执行完,自动释放。
堆内存 存储对象(包括数组对象),new来创建的,都存储在堆内存
方法区 存储已被虚拟机加载的类信息、常量、(静态变量)、即时编译器编译后的代码等数据。
本地方法栈 当程序中调用了native的本地方法时,本地方法执行期间的内存区域
程序计数器 程序计数器是CPU中的寄存器,它包含每一个线程下一条要执行的指令的地址

3.2 一维数组在内存中的存储

1、一个一维数组内存图

1
2
3
4
public static void main(String[] args) {
int[] arr = new int[3];
System.out.println(arr);//[I@5f150435
}

2、数组下标为什么是0开始

因为第一个元素距离数组首地址间隔0个单元格。

3、两个一维数组内存图

两个数组独立

1
2
3
4
5
6
public static void main(String[] args) {
int[] arr = new int[3];
int[] arr2 = new int[2];
System.out.println(arr);
System.out.println(arr2);
}

4、两个变量指向一个一维数组

两个数组变量本质上代表同一个数组。

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
// 定义数组,存储3个元素
int[] arr = new int[3];
//数组索引进行赋值
arr[0] = 5;
arr[1] = 6;
arr[2] = 7
//定义数组变量arr2,将arr的地址赋值给arr2
int[] arr2 = arr;
arr2[1] = 9;
System.out.println(arr[1]); // 输出9
}

4. 一维数组的应用

案例1:升景坊单间短期出租4个月,550元/月(水电煤公摊,网费35元/月),空调、卫生间、厨房齐全。屋内均是IT行业人士,喜欢安静。所以要求来租者最好是同行或者刚毕业的年轻人,爱干净、安静。

1
2
3
4
5
6
7
8
9
10
11
public class ArrayTest {
public static void main(String[] args) {
int[] arr = new int[]{8,2,1,0,3};
int[] index = new int[]{2,0,3,2,4,0,1,3,2,3,3};
String tel = "";
for(int i = 0;i < index.length;i++){
tel += arr[index[i]];
}
System.out.println("联系方式:" + tel);
}
}

案例2:输出英文星期几

用一个数组,保存星期一到星期天的7个英语单词,从键盘输入1-7,显示对应的单词
{“Monday”,”Tuesday”,”Wednesday”,”Thursday”,”Friday”,”Saturday”,”Sunday”}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.util.Scanner;

/**
* @author 尚硅谷-宋红康
* @create 14:37
*/
public class WeekArrayTest {
public static void main(String[] args) {

//1. 声明并初始化星期的数组
String[] weeks = {"Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"};

//2. 使用Scanner从键盘获取1-7范围的整数
Scanner scanner = new Scanner(System.in);
System.out.println("请输入[1-7]范围的整数:");
int number = scanner.nextInt();

if(number < 1 || number > 7){
System.out.println("你输入的输入非法");
}else{

//3. 根据输入的整数,到数组中相应的索引位置获取指定的元素(即:星期几)
System.out.println("对应的星期为:" + weeks[number - 1]);

}

scanner.close();

}
}

案例3:从键盘读入学生成绩,找出最高分,并输出学生成绩等级。

  • 成绩>=最高分-10 等级为’A’

  • 成绩>=最高分-20 等级为’B’

  • 成绩>=最高分-30 等级为’C’

  • 其余 等级为’D’

提示:先读入学生人数,根据人数创建int数组,存放学生成绩。

image-20220317004637748
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* @author 尚硅谷-宋红康
* @create 14:55
*/
public class ScoreTest1 {
public static void main(String[] args) {

//1. 根据提示,获取学生人数
System.out.print("请输入学生人数:");
Scanner scanner = new Scanner(System.in);
int count = scanner.nextInt();

//2. 根据学生人数,创建指定长度的数组 (使用动态初始化)
int[] scores = new int[count];

//3. 使用循环,依次给数组的元素赋值
int maxScore = 0; //记录最高分
System.out.println("请输入" + count + "个成绩");
for (int i = 0; i < scores.length; i++) {
scores[i] = scanner.nextInt();
//4. 获取数组中元素的最大值,即为最高分
if(maxScore < scores[i]){
maxScore = scores[i];
}
}

System.out.println("最高分是:" + maxScore);

//5. 遍历数组元素,输出各自的分数,并根据其分数与最高分的差值,获取各自的等级
char grade;
for (int i = 0; i < scores.length; i++) {

if(scores[i] >= maxScore - 10){
grade = 'A';
}else if(scores[i] >= maxScore - 20){
grade = 'B';
}else if(scores[i] >= maxScore - 30){
grade = 'C';
}else{
grade = 'D';
}
System.out.println("student " + i + " socre is " + scores[i] + ", grade is " + grade);
}
//关闭资源
scanner.close();

}
}

5. 多维数组的使用

5.1 概述

  • Java 语言里提供了支持多维数组的语法。
  • 如果说可以把一维数组当成几何中的线性图形,那么二维数组就相当于是一个表格,像Excel中的表格、围棋棋盘一样。

    image-20220317004810263
  • 应用举例1:

    某公司2022年全年各个月份的销售额进行登记。按月份存储,可以使用一维数组。如下:

    1
    int[] monthData = new int[]{23,43,22,34,55,65,44,67,45,78,67,66};

    如果改写为按季度为单位存储怎么办呢?

    1
    int[][] quarterData = new int[][]{{23,43,22},{34,55,65},{44,67,45},{78,67,66}};
  • 应用举例2:

高一年级三个班级均由多个学生姓名构成一个个数组。如下:

1
2
3
4
5
String[] class1 = new String[]{"段誉","令狐冲","任我行"};

String[] class2 = new String[]{"张三丰","周芷若"};

String[] class3 = new String[]{"赵敏","张无忌","韦小宝","杨过"};

那从整个年级看,我们可以声明一个二维数组。如下:

1
String[][] grade = new String[][]{{"段誉","令狐冲","任我行"},{"张三丰","周芷若"},{"赵敏","张无忌","韦小宝","杨过"}};
  • 应用举例3:

    image-20220516095701345

蓝框的几个元素,可以使用一维数组来存储。但现在发现每个元素下还有下拉框,其内部还有元素,那就需要使用二维数组来存储:

image-20220516095829526

  • 使用说明

    1561524724397

  • 对于二维数组的理解,可以看成是一维数组array1又作为另一个一维数组array2的元素而存在。
  • 其实,从数组底层的运行机制来看,其实没有多维数组。

5.2 声明与初始化

5.2.1 声明

二维数组声明的语法格式:

1
2
3
4
5
6
7
//推荐
元素的数据类型[][] 二维数组的名称;

//不推荐
元素的数据类型 二维数组名[][];
//不推荐
元素的数据类型[] 二维数组名[];

例如:

1
2
3
4
5
6
7
8
9
public class Test20TwoDimensionalArrayDefine {
public static void main(String[] args) {
//存储多组成绩
int[][] grades;

//存储多组姓名
String[][] names;
}
}

面试:

1
2
int[] x, y[];
//x是一维数组,y是二维数组 相当于 int[]x int[] y[];

5.2.2 静态初始化

格式:

1
int[][] arr = new int[][]{{3,8,2},{2,7},{9,0,1,6}};

定义一个名称为arr的二维数组,二维数组中有三个一维数组

  • 每一个一维数组中具体元素也都已初始化
    • 第一个一维数组 arr[0] = {3,8,2};
    • 第二个一维数组 arr[1] = {2,7};
    • 第三个一维数组 arr[2] = {9,0,1,6};
  • 第三个一维数组的长度表示方式:arr[2].length;
  • 注意特殊写法情况:int[] x,y[]; x是一维数组,y是二维数组。
  • 举例1:
1
2
3
4
5
6
7
8
int[][] arr = {{1,2,3},{4,5,6},{7,8,9,10}};//声明与初始化必须在一句完成

int[][] arr = new int[][]{{1,2,3},{4,5,6},{7,8,9,10}};

int[][] arr;
arr = new int[][]{{1,2,3},{4,5,6},{7,8,9,10}};

arr = new int[3][3]{{1,2,3},{4,5,6},{7,8,9,10}};//错误,静态初始化右边new 数据类型[][]中不能写数字
  • 举例2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TwoDimensionalArrayInitialize {
public static void main(String[] args) {
//存储多组成绩
int[][] grades = {
{89,75,99,100},
{88,96,78,63,100,86},
{56,63,58},
{99,66,77,88}
};

//存储多组姓名
String[][] names = {
{"张三","李四", "王五", "赵六"},
{"刘备","关羽","张飞","诸葛亮","赵云","马超"},
{"曹丕","曹植","曹冲"},
{"孙权","周瑜","鲁肃","黄盖"}
};
}
}

5.2.3 动态初始化

如果二维数组的每一个数据,甚至是每一行的列数,需要后期单独确定,那么就只能使用动态初始化方式了。动态初始化方式分为两种格式:

格式1:规则二维表:每一行的列数是相同的

1
2
3
4
5
6
7
8
9
//(1)确定行数和列数
元素的数据类型[][] 二维数组名 = new 元素的数据类型[m][n];
//其中,m:表示这个二维数组有多少个一维数组。或者说一共二维表有几行
//其中,n:表示每一个一维数组的元素有多少个。或者说每一行共有一个单元格

//此时创建完数组,行数、列数确定,而且元素也都有默认值

//(2)再为元素赋新值
二维数组名[行下标][列下标] = 值;

举例:

1
int[][] arr = new int[3][2];
  • 定义了名称为arr的二维数组

  • 二维数组中有3个一维数组

  • 每一个一维数组中有2个元素

  • 一维数组的名称分别为arr[0], arr[1], arr[2]

  • 给第一个一维数组1脚标位赋值为78写法是:arr[0][1] = 78;

格式2:不规则:每一行的列数不一样

1
2
3
4
5
6
7
8
9
10
11
12
//(1)先确定总行数
元素的数据类型[][] 二维数组名 = new 元素的数据类型[总行数][]; //行一定知道 但是列不一定知道

//此时只是确定了总行数,每一行里面现在是null

//(2)再确定每一行的列数,创建每一行的一维数组
二维数组名[行下标] = new 元素的数据类型[该行的总列数];

//此时已经new完的行的元素就有默认值了,没有new的行还是null

//(3)再为元素赋值
二维数组名[行下标][列下标] = 值;

举例:

1
int[][] arr = new int[3][];
  • 二维数组中有3个一维数组。
  • 每个一维数组都是默认初始化值null (注意:区别于格式1)
  • 可以对这个三个一维数组分别进行初始化:arr[0] = new int[3]; arr[1] = new int[1]; arr[2] = new int[2];
  • 注:int[][]arr = new int[][3]; //非法

练习:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/*
1
2 2
3 3 3
4 4 4 4
5 5 5 5 5
*/
public class Test25DifferentElementCount {
public static void main(String[] args){
//1、声明一个二维数组,并且确定行数
//因为每一行的列数不同,这里无法直接确定列数
int[][] arr = new int[5][];

//2、确定每一行的列数
for(int i=0; i<arr.length; i++){
/*
arr[0] 的列数是1
arr[1] 的列数是2
arr[2] 的列数是3
arr[3] 的列数是4
arr[4] 的列数是5
*/
arr[i] = new int[i+1];
}

//3、确定元素的值
for(int i=0; i<arr.length; i++){
for(int j=0; j<arr[i].length; j++){
arr[i][j] = i+1;
}
}

//4、遍历显示
for(int i=0; i<arr.length; i++){
for(int j=0; j<arr[i].length; j++){
System.out.print(arr[i][j] + " ");
}
System.out.println();
}

}
}

5.3 数组的长度和角标

  • 二维数组的长度/行数:二维数组名.length
  • 二维数组的某一行:二维数组名[行下标],此时相当于获取其中一组数据。它本质上是一个一维数组。行下标的范围:[0, 二维数组名.length-1]。此时把二维数组看成一维数组的话,元素是行对象。
  • 某一行的列数:二维数组名[行下标].length,因为二维数组的每一行是一个一维数组。
  • 某一个元素:二维数组名[行下标][列下标],即先确定行/组,再确定列。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Test22TwoDimensionalArrayUse {
public static void main(String[] args){
//存储3个小组的学员的成绩,分开存储,使用二维数组。
/*
int[][] scores1;
int scores2[][];
int[] scores3[];*/

int[][] scores = {
{85,96,85,75},
{99,96,74,72,75},
{52,42,56,75}
};

System.out.println(scores);//[[I@15db9742
System.out.println("一共有" + scores.length +"组成绩.");

//[[:代表二维数组,I代表元素类型是int
System.out.println(scores[0]);//[I@6d06d69c
//[:代表一维数组,I代表元素类型是int
System.out.println(scores[1]);//[I@7852e922
System.out.println(scores[2]);//[I@4e25154f
//System.out.println(scores[3]);//ArrayIndexOutOfBoundsException: 3

System.out.println("第1组有" + scores[0].length +"个学员.");
System.out.println("第2组有" + scores[1].length +"个学员.");
System.out.println("第3组有" + scores[2].length +"个学员.");

System.out.println("第1组的每一个学员成绩如下:");
//第一行的元素
System.out.println(scores[0][0]);//85
System.out.println(scores[0][1]);//96
System.out.println(scores[0][2]);//85
System.out.println(scores[0][3]);//75
//System.out.println(scores[0][4]);//java.lang.ArrayIndexOutOfBoundsException: 4
}
}

5.4 二维数组的遍历

  • 格式:
1
2
3
4
5
6
for(int i=0; i<二维数组名.length; i++){ //二维数组对象.length
for(int j=0; j<二维数组名[i].length; j++){//二维数组行对象.length
System.out.print(二维数组名[i][j]);
}
System.out.println();
}
  • 举例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test23TwoDimensionalArrayIterate {
public static void main(String[] args) {
//存储3个小组的学员的成绩,分开存储,使用二维数组。
int[][] scores = {
{85,96,85,75},
{99,96,74,72,75},
{52,42,56,75}
};

System.out.println("一共有" + scores.length +"组成绩.");
for (int i = 0; i < scores.length; i++) {
System.out.print("第" + (i+1) +"组有" + scores[i].length + "个学员,成绩如下:");
for (int j = 0; j < scores[i].length; j++) {
System.out.print(scores[i][j]+"\t");
}
System.out.println();
}
}
}

5.5 内存解析

二维数组本质上是元素类型是一维数组的一维数组。

1
2
3
4
5
6
7
int[][] arr = {
{1},
{2,2},
{3,3,3},
{4,4,4,4},
{5,5,5,5,5}
};

1562112672215

1
2
3
4
5
6
7
8
9
//1、声明二维数组,并确定行数和列数
int[][] arr = new int[4][5];

//2、确定元素的值
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr.length; j++) {
arr[i][j] = i + 1;
}
}

1562113179785

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//1、声明一个二维数组,并且确定行数
//因为每一行的列数不同,这里无法直接确定列数
int[][] arr = new int[5][];

//2、确定每一行的列数
for(int i=0; i<arr.length; i++){
/*
arr[0] 的列数是1
arr[1] 的列数是2
arr[2] 的列数是3
arr[3] 的列数是4
arr[4] 的列数是5
*/
arr[i] = new int[i+1];
}

//3、确定元素的值
for(int i=0; i<arr.length; i++){
for(int j=0; j<arr[i].length; j++){
arr[i][j] = i+1;
}
}

1562113981079

5.6 应用举例

案例1:获取arr数组中所有元素的和。

提示:使用for的嵌套循环即可。

image-20220317005436209

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Exercise3 {
public static void main(String[] args){
int[][] arr=new int[][]{{3,5,8,0},{12,9,0,0},{7,0,6,4}};
int sum=0;
System.out.println(arr.length); //行的长度
System.out.println(arr[0].length); //列的长度
//两层循环
for(int i=0;i<arr.length;i++){
for(int j=0;j<arr[0].length;j++){
sum+=arr[i][j];
}
}
System.out.println(sum);
}
}

案例2:声明:int[] x,y[]; 在给x,y变量赋值以后,以下选项允许通过编译的是:

1
2
3
4
5
6
7
8
9
10
11
声明:int[] x,y[]; 在给x,y变量赋值以后,以下选项允许通过编译的是:
a) x[0] = y; //no
b) y[0] = x; //yes
c) y[0][0] = x; //no
d) x[0][0] = y; //no
e) y[0][0] = x[0]; //yes
f) x = y; //no

提示:
一维数组:int[] x 或者int x[]
二维数组:int[][] y 或者 int[] y[] 或者 int y[][]

案例3:使用二维数组打印一个 10 行杨辉三角。

提示:

  1. 第一行有 1 个元素, 第 n 行有 n 个元素

  2. 每一行的第一个元素和最后一个元素都是 1

  3. 从第三行开始, 对于非第一个元素和最后一个元素的元素。即:

    1
    yanghui[i][j] = yanghui[i-1][j-1] + yanghui[i-1][j];

image-20220317005549522

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* @author 尚硅谷-宋红康
* @create 10:11
*/
public class YangHuiTest {
public static void main(String[] args) {

int[][] arr = new int[10][];
for (int i = 0; i < arr.length; i++) {
arr[i] = new int[i+1]; //每一行都多一列
//每一行的第一个元素和对角线元素都是1
arr[i][0] = arr[i][i] = 1;
for (int j = 1; j < arr[i].length - 1; j++) {
arr[i][j] = arr[i - 1][j - 1] + arr[i - 1][j];
}
}

//输出二维数组
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i].length; j++) {
System.out.print(arr[i][j] + "\t");
}
System.out.println(); //每一行换行
}

}
}

6. 数组的常见算法

6.1 数值型数组特征值统计

  • 这里的特征值涉及到:平均值、最大值、最小值、总和等

举例1:数组统计:求总和、均值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TestArrayElementSum {
public static void main(String[] args) {
int[] arr = {4,5,6,1,9};
//求总和、均值
int sum = 0;//因为0加上任何数都不影响结果
for(int i=0; i<arr.length; i++){
sum += arr[i];
}
double avg = (double)sum/arr.length;

System.out.println("sum = " + sum);
System.out.println("avg = " + avg);
}
}

举例2:求数组元素的总乘积

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestArrayElementMul {
public static void main(String[] args) {
int[] arr = {4,5,6,1,9};

//求总乘积
long result = 1;//因为1乘以任何数都不影响结果
for(int i=0; i<arr.length; i++){
result *= arr[i];
}

System.out.println("result = " + result);
}
}

举例3:求数组元素中偶数的个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TestArrayElementEvenCount {
public static void main(String[] args) {
int[] arr = {4,5,6,1,9};
//统计偶数个数
int evenCount = 0;
for(int i=0; i<arr.length; i++){
if(arr[i]%2==0){
evenCount++;
}
}

System.out.println("evenCount = " + evenCount);
}
}

举例4:求数组元素的最大值

1574577970893

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class TestArrayMax {
public static void main(String[] args) {
int[] arr = {4,5,6,1,9};
//找最大值
int max = arr[0];
for(int i=1; i<arr.length; i++){//此处i从1开始,是max不需要与arr[0]再比较一次了
if(arr[i] > max){
max = arr[i];
}
}

System.out.println("max = " + max);
}
}

举例5:找最值及其第一次出现的下标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestMaxIndex {
public static void main(String[] args) {
int[] arr = {4,5,6,1,9};
//找最大值以及第一个最大值下标
int max = arr[0];
int index = 0;
for(int i=1; i<arr.length; i++){
if(arr[i] > max){
max = arr[i];
index = i;
}
}

System.out.println("max = " + max);
System.out.println("index = " + index);
}
}

举例6:找最值及其所有最值的下标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Test13AllMaxIndex {
public static void main(String[] args) {
int[] arr = {4,5,6,1,9,9,3};
//找最大值
int max = arr[0];
for(int i=1; i<arr.length; i++){
if(arr[i] > max){
max = arr[i];
}
}
System.out.println("最大值是:" + max);
System.out.print("最大值的下标有:");

//遍历数组,看哪些元素和最大值是一样的
for(int i=0; i<arr.length; i++){
if(max == arr[i]){
System.out.print(i+"\t");
}
}
System.out.println();
}
}

优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test13AllMaxIndex2 {
public static void main(String[] args) {
int[] arr = {4,5,6,1,9,9,3};
//找最大值
int max = arr[0];
String index = "0";
for(int i=1; i<arr.length; i++){
if(arr[i] > max){
max = arr[i];
index = i + "";
}else if(arr[i] == max){
index += "," + i;
}
}

System.out.println("最大值是" + max);
System.out.println("最大值的下标是[" + index+"]");
}
}

举例7(难):输入一个整形数组,数组里有正数也有负数。数组中连续的一个或多个整数组成一个子数组,每个子数组都有一个和。求所有子数组的和的最大值。要求时间复杂度为O(n)。
例如:输入的数组为1, -2, 3, -10, -4, 7, 2, -5,和最大的子数组为3, 10, -4, 7, 2,因此输出为该子数组的和18。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Test5 {
public static void main(String[] args) {
int[] arr = new int[]{1, -2, 3, 10, -4, 7, 2, -5};
int i = getGreatestSum(arr);
System.out.println(i);
}

public static int getGreatestSum(int[] arr){
int greatestSum = 0;
if(arr == null || arr.length == 0){
return 0;
}
int temp = greatestSum;
for(int i = 0;i < arr.length;i++){
temp += arr[i];

if(temp < 0){
temp = 0;
}

if(temp > greatestSum){
greatestSum = temp;
}
}
if(greatestSum == 0){
greatestSum = arr[0];
for(int i = 1;i < arr.length;i++){
if(greatestSum < arr[i]){
greatestSum = arr[i];
}
}
}
return greatestSum;
}
}

举例8:评委打分

分析以下需求,并用代码实现:

(1)在编程竞赛中,有10位评委为参赛的选手打分,分数分别为:5,4,6,8,9,0,1,2,7,3

(2)求选手的最后得分(去掉一个最高分和一个最低分后其余8位评委打分的平均值)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* @author 尚硅谷-宋红康
* @create 10:03
*/
public class ArrayExer {
public static void main(String[] args) {
int[] arr={5,4,6,8,9,0,1,2,7,3};
//最高分
int max=0;
for(int i=0;i<arr.length;i++){
if(arr[i]>max){
max=arr[i];
}
}
//最低分
int min=arr[0];
for(int i=0;i<arr.length;i++){
if(arr[i]<min)
min=arr[i];
}
//算平均分 [所有的-MAX-MIN再除以总数-2]
int average=0;
for(int i=0;i<arr.length;i++){
average+=arr[i];
}
System.out.println((double)(average-max-min)/(arr.length-2)); //要考虑最终结果可能是一个浮点数
}
}

6.2 数组元素的赋值与数组复制

举例1:杨辉三角(见二维数组课后案例)

举例2:使用简单数组

(1)创建一个名为ArrayTest的类,在main()方法中声明array1和array2两个变量,他们是int[]类型的数组。

(2)使用大括号{},把array1初始化为8个素数:2,3,5,7,11,13,17,19。

(3)显示array1的内容。

(4)赋值array2变量等于array1,修改array2中的偶索引元素,使其等于索引值(如array[0]=0,array[2]=2)。打印出array1。 array2 = array1;

思考:array1和array2是什么关系?

拓展:修改题目,实现array2对array1数组的复制

snipaste_20220317_225346snipaste_20220317_225359

举例3:一个数组,让数组的每个元素去除第一个元素,得到的商作为被除数所在位置的新值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test3 {
public static void main(String[] args) {
int[] arr = new int[]{12,43,65,3,-8,64,2};

// for(int i = 0;i < arr.length;i++){
// arr[i] = arr[i] / arr[0];
// }
for(int i = arr.length -1;i >= 0;i--){
arr[i] = arr[i] / arr[0];
}
//遍历arr
for(int i = 0;i < arr.length;i++){
System.out.print(arr[i] + " ");
}
}
}

举例4:创建一个长度为6的int型数组,要求数组元素的值都在1-30之间,且是随机赋值。同时,要求元素的值各不相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class Test4 {
// 5-67 Math.random() * 63 + 5;
@Test
public void test1() {
int[] arr = new int[6];
for (int i = 0; i < arr.length; i++) {// [0,1) [0,30) [1,31)
arr[i] = (int) (Math.random() * 30) + 1;

boolean flag = false;
while (true) {
for (int j = 0; j < i; j++) {
if (arr[i] == arr[j]) {
flag = true;
break;
}
}
if (flag) {
arr[i] = (int) (Math.random() * 30) + 1;
flag = false;
continue;
}
break;
}
}

for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
//更优的方法
@Test
public void test2(){
int[] arr = new int[6];
for (int i = 0; i < arr.length; i++) {// [0,1) [0,30) [1,31)
arr[i] = (int) (Math.random() * 30) + 1;

for (int j = 0; j < i; j++) {
if (arr[i] == arr[j]) {
i--;
break;
}
}
}

for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
}

举例5:扑克牌

案例:遍历扑克牌

遍历扑克牌,效果如图所示:

1659199523932

提示:使用两个字符串数组,分别保存花色和点数,再用一个字符串数组保存最后的扑克牌。
String[] hua = {“黑桃”,”红桃”,”梅花”,”方片”};
String[] dian = {“A”,”2”,”3”,”4”,”5”,”6”,”7”,”8”,”9”,”10”,”J”,”Q”,”K”};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.atguigu3.common_algorithm.exer5;

/**
* @author 尚硅谷-宋红康
* @create 17:16
*/
public class ArrayExer05 {
public static void main(String[] args) {
String[] hua = {"黑桃","红桃","梅花","方片"};
String[] dian = {"A","2","3","4","5","6","7","8","9","10","J","Q","K"};


String[] pai = new String[hua.length * dian.length];
int k = 0;
for(int i = 0;i < hua.length;i++){
for(int j = 0;j < dian.length;j++){
pai[k++] = hua[i] + dian[j]; //添加花色和字母
}
}

for (int i = 0; i < pai.length; i++) {
System.out.print(pai[i] + " ");
if(i % 13 == 12){
System.out.println(); //12个为一行
}
}

}
}

拓展:在上述基础上,增加大王、小王。

举例6:回形数

从键盘输入一个整数(1~20) ,则以该数字为矩阵的大小,把1,2,3…n*n 的数字按照顺时针螺旋的形式填入其中。

例如: 输入数字2,则程序输出:
1 2
4 3

输入数字3,则程序输出:
1 2 3
8 9 4
7 6 5
输入数字4, 则程序输出:
1 2 3 4
12 13 14 5
11 16 15 6
10 9 8 7

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
//方式1
public class RectangleTest {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("输入一个数字");
int len = scanner.nextInt();
int[][] arr = new int[len][len];

int s = len * len;
/*
* k = 1:向右
* k = 2:向下
* k = 3:向左
* k = 4:向上
*/
int k = 1;
int i = 0,j = 0;
for(int m = 1;m <= s;m++){
if(k == 1){
if(j < len && arr[i][j] == 0){
arr[i][j++] = m;
}else{
k = 2;
i++;
j--;
m--;
}
}else if(k == 2){
if(i < len && arr[i][j] == 0){
arr[i++][j] = m;
}else{
k = 3;
i--;
j--;
m--;
}
}else if(k == 3){
if(j >= 0 && arr[i][j] == 0){
arr[i][j--] = m;
}else{
k = 4;
i--;
j++;
m--;
}
}else if(k == 4){
if(i >= 0 && arr[i][j] == 0){
arr[i--][j] = m;
}else{
k = 1;
i++;
j++;
m--;
}
}
}

//遍历
for(int m = 0;m < arr.length;m++){
for(int n = 0;n < arr[m].length;n++){
System.out.print(arr[m][n] + "\t");
}
System.out.println();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//方式2
/*
01 02 03 04 05 06 07
24 25 26 27 28 29 08
23 40 41 42 43 30 09
22 39 48 49 44 31 10
21 38 47 46 45 32 11
20 37 36 35 34 33 12
19 18 17 16 15 14 13
*/
public class RectangleTest1 {

public static void main(String[] args) {
int n = 7;
int[][] arr = new int[n][n];

int count = 0; //要显示的数据
int maxX = n-1; //x轴的最大下标
int maxY = n-1; //Y轴的最大下标
int minX = 0; //x轴的最小下标
int minY = 0; //Y轴的最小下标
while(minX<=maxX){
for(int x=minX;x<=maxX;x++) {
arr[minY][x] = ++count; //横着从左往右 01开始
}
minY++; //下一循环就是下一行的横
for(int y=minY;y<=maxY;y++) {
arr[y][maxX] = ++count; //竖着从上往下 08开始
}
maxX--; //往前挪一个
for(int x=maxX;x>=minX;x--) {
arr[maxY][x] = ++count; //横着从右往左 14开始
}
maxY--;
for(int y=maxY;y>=minY;y--) {
arr[y][minX] = ++count; //竖着从下往上 20开始
}
minX++; //下一个循环就是下一行
}


for(int i=0;i<arr.length;i++) {
for(int j=0;j<arr.length;j++) {
String space = (arr[i][j]+"").length()==1 ? "0":"";
System.out.print(space+arr[i][j]+" ");
}
System.out.println();
}
}
}

6.3 数组元素的反转

实现思想:数组对称位置的元素互换。

image-20221117195931777
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class TestArrayReverse1 {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5};
System.out.println("反转之前:");
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}

//反转
/*
思路:首尾对应位置的元素交换
(1)确定交换几次
次数 = 数组.length / 2
(2)谁和谁交换
for(int i=0; i<次数; i++){
int temp = arr[i];
arr[i] = arr[arr.length-1-i];
arr[arr.length-1-i] = temp;
}
*/
for(int i=0; i<arr.length/2; i++){
int temp = arr[i];
arr[i] = arr[arr.length-1-i];
arr[arr.length-1-i] = temp;
}

System.out.println("反转之后:");
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}

}

1561469087319

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class TestArrayReverse2 {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6,7,8,9};
System.out.println("反转之前:");
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]+" ");
}
System.out.println();

for(int left=0,right=arr.length-1;left<=right;left++,right--){
//两个数做交换
int temp=0;
temp=arr[left];
arr[left]=arr[right];
arr[right]=temp;
}

System.out.println("反转之后:");
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]+" ");
}
}
}

6.4 数组的扩容与缩容

数组的扩容

题目:现有数组 int[] arr = new int[]{1,2,3,4,5}; ,现将数组长度扩容1倍,并将10,20,30三个数据添加到arr数组中,如何操作?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ArrTest1 {
public static void main(String[] args) {

int[] arr = new int[]{1,2,3,4,5};
int[] newArr = new int[arr.length << 1]; //位运算符相当于乘2

for(int i = 0;i < arr.length;i++){
newArr[i] = arr[i];
}

newArr[arr.length] = 10;
newArr[arr.length + 1] = 20;
newArr[arr.length + 2] = 30;

arr = newArr;

//遍历arr
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
}

数组的缩容

题目:现有数组 int[] arr={1,2,3,4,5,6,7}。现需删除数组中索引为4的元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ArrTest2 {
public static void main(String[] args) {

int[] arr = {1, 2, 3, 4, 5, 6, 7};
//删除数组中索引为4的元素
int delIndex = 4;
//方案1:
/*//创建新数组
int[] newArr = new int[arr.length - 1];

for (int i = 0; i < delIndex; i++) {
newArr[i] = arr[i];
}
for (int i = delIndex + 1; i < arr.length; i++) {
newArr[i - 1] = arr[i];
}

arr = newArr;
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}*/

//方案2:
for(int i=delIndex-1;i<arr.length-1;i++){
System.out.println("本轮要替换的是"+arr[i]+" "+arr[i+1]);
arr[i]=arr[i+1];
}
//只遍历前6个
for (int i = 0; i < arr.length-1; i++) {
System.out.println(arr[i]);
}
}
}

6.5 数组的元素查找

1、顺序查找

顺序查找:挨个查看

要求:对数组元素的顺序没要求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TestArrayOrderSearch {
//查找value第一次在数组中出现的index
public static void main(String[] args){
int[] arr = {4,5,6,1,9};
int value = 1;
int index = -1;

for(int i=0; i<arr.length; i++){
if(arr[i] == value){
index = i;
break;
}
}

if(index==-1){
System.out.println(value + "不存在");
}else{
System.out.println(value + "的下标是" + index);
}
}
}

2、二分查找

举例:

image-20220317230955644

实现步骤:

image-20220623210601915
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//二分法查找:要求此数组必须是有序的。
int[] arr3 = new int[]{-99,-54,-2,0,2,33,43,256,999};
boolean isFlag = true;
int value = 256;
//int value = 25;
int head = 0;//首索引位置
int end = arr3.length - 1;//尾索引位置
while(head <= end){
int middle = (head + end) / 2;
if(arr3[middle] == value){
System.out.println("找到指定的元素,索引为:" + middle);
isFlag = false;
break;
}else if(arr3[middle] > value){
end = middle - 1;
}else{//arr3[middle] < value
head = middle + 1;
}
}

if(isFlag){
System.out.println("未找打指定的元素");
}

6.6 数组元素排序

6.6.1 算法概述

  • 定义

    • 排序:假设含有n个记录的序列为{R1,R2,…,Rn},其相应的关键字序列为{K1,K2,…,Kn}。将这些记录重新排序为{Ri1,Ri2,…,Rin},使得相应的关键字值满足条Ki1<=Ki2<=…<=Kin,这样的一种操作称为排序。
    • 通常来说,排序的目的是快速查找。
  • 衡量排序算法的优劣:

    • 时间复杂度:分析关键字的比较次数和记录的移动次数

    • 常见的算法时间复杂度由小到大依次为:Ο(1)<Ο(log2n)<Ο(n)<Ο(nlog2n)<Ο(n2)<Ο(n3)<…<Ο(2n)<Ο(n!)<O(nn)

    • 空间复杂度:分析排序算法中需要多少辅助内存

      1
      一个算法的空间复杂度S(n)定义为该算法所耗费的存储空间,它也是问题规模n的函数。
    • 稳定性:若两个记录A和B的关键字值相等,但排序后A、B的先后次序保持不变,则称这种排序算法是稳定的。

      image-20211222113701365

6.6.2 排序算法概述

  • 排序算法分类:内部排序和外部排序

    • 内部排序:整个排序过程不需要借助于外部存储器(如磁盘等),所有排序操作都在内存中完成。
    • 外部排序:参与排序的数据非常多,数据量非常大,计算机无法把整个排序过程放在内存中完成,必须借助于外部存储器(如磁盘)。外部排序最常见的是多路归并排序。可以认为外部排序是由多次内部排序组成。
  • 十大内部排序算法

​ 数组的排序算法很多,实现方式各不相同,时间复杂度、空间复杂度、稳定性也各不相同:

image-20211222111142684

常见时间复杂度所消耗的时间从小到大排序:

O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)

注意,经常将以2为底n的对数简写成logn。

image-20220824003440106

6.6.3 冒泡排序(Bubble Sort)

image-20220516094637228

排序思想:

  1. 比较相邻的元素。如果第一个比第二个大(升序),就交换他们两个。

  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。

  3. 针对所有的元素重复以上的步骤,除了最后一个。

  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较为止。

BubbleSort

动态演示:https://visualgo.net/zh/sorting

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/*
1、冒泡排序(最经典)
思想:每一次比较“相邻(位置相邻)”元素,如果它们不符合目标顺序(例如:从小到大),
就交换它们,经过多轮比较,最终实现排序。
(例如:从小到大) 每一轮可以把最大的沉底,或最小的冒顶。

过程:arr{6,9,2,9,1} 目标:从小到大

第一轮:
第1次,arr[0]与arr[1],6>9不成立,满足目标要求,不交换
第2次,arr[1]与arr[2],9>2成立,不满足目标要求,交换arr[1]与arr[2] {6,2,9,9,1}
第3次,arr[2]与arr[3],9>9不成立,满足目标要求,不交换
第4次,arr[3]与arr[4],9>1成立,不满足目标要求,交换arr[3]与arr[4] {6,2,9,1,9}
第一轮所有元素{6,9,2,9,1}已经都参与了比较,结束。
第一轮的结果:第“一”最大值9沉底(本次是后面的9沉底),即到{6,2,9,1,9}元素的最右边

第二轮:
第1次,arr[0]与arr[1],6>2成立,不满足目标要求,交换arr[0]与arr[1] {2,6,9,1,9}
第2次,arr[1]与arr[2],6>9不成立,满足目标要求,不交换
第3次:arr[2]与arr[3],9>1成立,不满足目标要求,交换arr[2]与arr[3] {2,6,1,9,9}
第二轮未排序的所有元素 {6,2,9,1}已经都参与了比较,结束。
第二轮的结果:第“二”最大值9沉底(本次是前面的9沉底),即到{2,6,1,9}元素的最右边
第三轮:
第1次,arr[0]与arr[1],2>6不成立,满足目标要求,不交换
第2次,arr[1]与arr[2],6>1成立,不满足目标要求,交换arr[1]与arr[2] {2,1,6,9,9}
第三轮未排序的所有元素{2,6,1}已经都参与了比较,结束。
第三轮的结果:第三最大值6沉底,即到 {2,1,6}元素的最右边
第四轮:
第1次,arr[0]与arr[1],2>1成立,不满足目标要求,交换arr[0]与arr[1] {1,2,6,9,9}
第四轮未排序的所有元素{2,1}已经都参与了比较,结束。
第四轮的结果:第四最大值2沉底,即到{1,2}元素的最右边

*/
public class Test19BubbleSort{
public static void main(String[] args){
int[] arr = {6,9,2,9,1};

//目标:从小到大
//冒泡排序的轮数 = 元素的总个数 - 1
//轮数是多轮,每一轮比较的次数是多次,需要用到双重循环,即循环嵌套
//外循环控制 轮数,内循环控制每一轮的比较次数和过程
for(int i=1; i<arr.length; i++){ //循环次数是arr.length-1次/轮
/*
假设arr.length=5
i=1,第1轮,比较4次
arr[0]与arr[1]
arr[1]与arr[2]
arr[2]与arr[3]
arr[3]与arr[4]

arr[j]与arr[j+1],int j=0;j<4; j++

i=2,第2轮,比较3次
arr[0]与arr[1]
arr[1]与arr[2]
arr[2]与arr[3]

arr[j]与arr[j+1],int j=0;j<3; j++

i=3,第3轮,比较2次
arr[0]与arr[1]
arr[1]与arr[2]

arr[j]与arr[j+1],int j=0;j<2; j++
i=4,第4轮,比较1次
arr[0]与arr[1]

arr[j]与arr[j+1],int j=0;j<1; j++

int j=0; j<arr.length-i; j++
*/
for(int j=0; j<arr.length-i; j++){
//希望的是arr[j] < arr[j+1]
if(arr[j] > arr[j+1]){
//交换arr[j]与arr[j+1]
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}

//完成排序,遍历结果
for(int i=0; i<arr.length; i++){
System.out.print(arr[i]+" ");
}
}
}

冒泡排序优化(选讲)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/*
思考:冒泡排序是否可以优化
*/
class Test19BubbleSort2{
public static void main(String[] args) {
int[] arr = {1, 3, 5, 7, 9};

//从小到大排序
for (int i = 0; i < arr.length - 1; i++) {
boolean flag = true;//假设数组已经是有序的
for (int j = 0; j < arr.length - 1 - i; j++) {
//希望的是arr[j] < arr[j+1]
if (arr[j] > arr[j + 1]) {
//交换arr[j]与arr[j+1]
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;

flag = false;//如果元素发生了交换,那么说明数组还没有排好序
}
}
if (flag) {
break;
}
}

//完成排序,遍历结果
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
}

6.6.4 快速排序

快速排序(Quick Sort)由图灵奖获得者Tony Hoare发明,被列为20世纪十大算法之一,是迄今为止所有内排序算法中速度最快的一种,快速排序的时间复杂度为O(nlog(n))。

快速排序通常明显比同为O(nlogn)的其他算法更快,因此常被采用,而且快排采用了分治法的思想,所以在很多笔试面试中能经常看到快排的影子。

排序思想:

  1. 从数列中挑出一个元素,称为”基准”(pivot),

  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。

  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

  4. 递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

动态演示:https://visualgo.net/zh/sorting

图示1:

image-20220317235922776

图示2:

第一轮操作:

image-20221117205612230

第二轮操作:

image-20221117205719427

6.6.5 内部排序性能比较与选择

  • 性能比较

    • 从平均时间而言:快速排序最佳。但在最坏情况下时间性能不如堆排序和归并排序。
    • 从算法简单性看:由于直接选择排序、直接插入排序和冒泡排序的算法比较简单,将其认为是简单算法。对于Shell排序、堆排序、快速排序和归并排序算法,其算法比较复杂,认为是复杂排序。
    • 从稳定性看:直接插入排序、冒泡排序和归并排序时稳定的;而直接选择排序、快速排序、 Shell排序和堆排序是不稳定排序
    • 从待排序的记录数n的大小看,n较小时,宜采用简单排序;而n较大时宜采用改进排序。
  • 选择

    • 若n较小(如n≤50),可采用直接插入或直接选择排序。
      当记录规模较小时,直接插入排序较好;否则因为直接选择移动的记录数少于直接插入,应选直接选择排序为宜。
    • 若文件初始状态基本有序(指正序),则应选用直接插入、冒泡或随机的快速排序为宜;
    • 若n较大,则应采用时间复杂度为O(nlgn)的排序方法:快速排序、堆排序或归并排序。

7. Arrays工具类的使用

java.util.Arrays类即为操作数组的工具类,包含了用来操作数组(比如排序和搜索)的各种方法。 比如:

  • 数组元素拼接
    • static String toString(int[] a) :字符串表示形式由数组的元素列表组成,括在方括号(”[]”)中。相邻元素用字符 “, “(逗号加空格)分隔。形式为:[元素1,元素2,元素3。。。]
    • static String toString(Object[] a) :字符串表示形式由数组的元素列表组成,括在方括号(”[]”)中。相邻元素用字符 “, “(逗号加空格)分隔。元素将自动调用自己从Object继承的toString方法将对象转为字符串进行拼接,如果没有重写,则返回类型@hash值,如果重写则按重写返回的字符串进行拼接。
  • 数组排序 sort
    • static void sort(int[] a) :将a数组按照从小到大进行排序
    • static void sort(int[] a, int fromIndex, int toIndex) :将a数组的[fromIndex, toIndex)部分按照升序排列
    • static void sort(Object[] a) :根据元素的自然顺序对指定对象数组按升序进行排序。
    • static void sort(T[] a, Comparator<? super T> c) :根据指定比较器产生的顺序对指定对象数组进行排序。
  • 数组元素的二分查找 binarySearch
    • static int binarySearch(int[] a, int key) 、static int binarySearch(Object[] a, Object key) :要求数组有序,在数组中查找key是否存在,如果存在返回第一次找到的下标,不存在返回负数。
  • 数组的复制 copyof
    • static int[] copyOf(int[] original, int newLength) :根据original原数组复制一个长度为newLength的新数组,并返回新数组
    • static T[] copyOf(T[] original,int newLength):根据original原数组复制一个长度为newLength的新数组,并返回新数组
    • static int[] copyOfRange(int[] original, int from, int to) :复制original原数组的[from,to)构成新数组,并返回新数组
    • static T[] copyOfRange(T[] original,int from,int to):复制original原数组的[from,to)构成新数组,并返回新数组
  • 比较两个数组是否相等 equals
    • static boolean equals(int[] a, int[] a2) :比较两个数组的长度、元素是否完全相同
    • static boolean equals(Object[] a,Object[] a2):比较两个数组的长度、元素是否完全相同
  • 填充数组 fill
    • static void fill(int[] a, int val) :用val值填充整个a数组
    • static void fill(Object[] a,Object val):用val对象填充整个a数组
    • static void fill(int[] a, int fromIndex, int toIndex, int val):将a数组[fromIndex,toIndex)部分填充为val值
    • static void fill(Object[] a, int fromIndex, int toIndex, Object val) :将a数组[fromIndex,toIndex)部分填充为val对象

举例:java.util.Arrays类的sort()方法提供了数组元素排序功能:

1
2
3
4
5
6
7
8
9
import java.util.Arrays;
public class SortTest {
public static void main(String[] args) {
int[] arr = {3, 2, 5, 1, 6};
System.out.println("排序前" + Arrays.toString(arr));
Arrays.sort(arr);
System.out.println("排序后" + Arrays.toString(arr));
}
}

8. 数组中的常见异常

8.1 数组角标越界异常

当访问数组元素时,下标指定超出[0, 数组名.length-1]的范围时,就会报数组下标越界异常:ArrayIndexOutOfBoundsException。

1
2
3
4
5
6
7
8
public class TestArrayIndexOutOfBoundsException {
public static void main(String[] args) {
int[] arr = {1,2,3};
// System.out.println("最后一个元素:" + arr[3]);//错误,下标越界
// System.out.println("最后一个元素:" + arr[arr.length]);//错误,下标越界
System.out.println("最后一个元素:" + arr[arr.length-1]);//对
}
}

创建数组,赋值3个元素,数组的索引就是0,1,2,没有3索引,因此我们不能访问数组中不存在的索引,程序运行后,将会抛出 ArrayIndexOutOfBoundsException 数组越界异常。在开发中,数组的越界异常是不能出现的,一旦出现了,就必须要修改我们编写的代码。

8.2 空指针异常

观察一下代码,运行后会出现什么结果。

1
2
3
4
5
6
7
8
public class TestNullPointerException {
public static void main(String[] args) {
//定义数组
int[][] arr = new int[3][];

System.out.println(arr[0][0]);//NullPointerException
}
}

因为此时数组的每一行还未分配具体存储元素的空间,此时arr[0]是null,此时访问arr[0][0]会抛出NullPointerException 空指针异常。

空指针异常在内存图中的表现

1572338767825

小结:空指针异常情况

1
2
3
4
5
6
7
8
9
10
11
12
13
		//举例一:
// int[] arr1 = new int[10];
// arr1 = null;
// System.out.println(arr1[9]);

//举例二:
// int[][] arr2 = new int[5][];
// //arr2[3] = new int[10];
// System.out.println(arr2[3][3]);

//举例三:
String[] arr3 = new String[10];
System.out.println(arr3[2].toString());

pytorch小土堆

学习链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14

1.哔哩哔哩视频
https://www.bilibili.com/video/BV1hE411t7RN?p=18&spm_id_from=pageDriver&vd_source=f2ebbaf7e4283edae08088dbbbaff299

2.pytorch官网
https://pytorch.org/docs/stable/generated/torch.nn.functional.conv2d.html#torch.nn.functional.conv2d

3.小土堆的资料链接
土堆B站视频课件:https://pan.baidu.com/wap/init?surl=moZb_eKmVCcRHS49IPKHQw#/ 提取码:t3st

4.我自己电脑是tuduipytorch2环境(python3.8)

5.配置pytorch用的链接里面的11.3版本
https://blog.csdn.net/weixin_45334223/article/details/128772572?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522168974940216800227485583%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=168974940216800227485583&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~top_positive~default-2-128772572-null-null.268^v1^control&utm_term=%E5%AE%89%E8%A3%85pytorch&spm=1018.2226.3001.4450

p16神经网络的基本骨架(nn.Module)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

import torch
from torch import nn

class Tudui(nn.Module):
def __init__(self):
super().__init__() #重写父类

def forward(self, input):
output = input + 1 #计数
return output

tudui = Tudui()
x = torch.tensor(1.0)
output = tudui(x)
print(output)


p17卷积操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

import torch
import torch.nn.functional as F

#输入图像 5*5
input=torch.tensor([[1,2,0,3,1],
[0,1,2,3,1],
[1,2,1,0,0],
[5,2,3,1,1],
[2,1,0,1,1]])
#卷积核 3*3
kernel=torch.tensor([1,2,1],
[0,1,0],
[2,1,0])

#将输入图像更改为1个数据1个通道 高度5宽度5 input要求为(minibatch,in_channels,iH,iW)
input=torch.reshape(input,(1,1,5,5))
kernel=kernel.reshape(input,(1,1,3,3))

print(input.shape)
print(kernel.shape)

#步长为1 就是每次移动一位
output=F.conv2d(input,kernel,stride=1)
print(output)

output2=F.conv2d(input,kernel,stride=2)
print(output2)

#填充为1
output3=F.conv2d(input,kernel,stride=1,padding=1)
print(output3)


p18卷积层

动画
https://github.com/vdumoulin/conv_arithmetic/blob/master/README.md

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

import torch
import torchvision
from torch.nn import Conv2d
from torch.utils.data import DataLoader
from torch import nn
dataset=torchvision.datasets.CIFAR10("../data",train=False,transform=torchvision.transforms.ToTensor(),download=True)
dataloader=DataLoader(dataset,batch_size=64)

class Tudui(nn.Module):
def __init__(self):
super(Tudui,self).__init__() #重写父类
self.conv1=Conv2d(in_channels=3,out_channels=6,kernel_size=3,stride=1,padding=0) #设置一个卷积层

def forward(self,x):
x=self.conv1(x)
return x

tudui=Tudui()
print(tudui) #conv1:conv2d(3,6,kernel_size=(3,3),stride=(1,1))

for data in dataloader:
imgs,target=data
output=tudui(imgs) #把数据集的照片放进去
#图片开始的格式
print(imgs.shape) #【64,4,32,32】
#图片最终的格式
print(output.shape)#【64,3,30,30】


p19最大池化层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

import torch
from torch import nn
from torch.nn import MaxPool2d

#输入图像 5*5
input=torch.tensor([[1,2,0,3,1],
[0,1,2,3,1],
[1,2,1,0,0],
[5,2,3,1,1],
[2,1,0,1,1]],dtype=torch.float32)

input = torch.reshape(input, (-1, 1, 5, 5)) #-1就是根据其他设置,自动计算
print(input)
print(input.shape)

class Tudui(nn.Module):
def __init__(self):
super(Tudui,self).__init__() #重写父类
#池化层就是减少参数 让模型更简略更小
self.maxpool1=MaxPool2d(kernel_size=3,ceil_mode=True) #设置一个池化层 ceil_mode=True能够保证不够池化核大小的时候可以出结果

def forward(self,input):
output=self.maxpool1(input)
return output

tudui=Tudui()
output=tudui(input)
print(output)


p20非线性激活

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

#主要介绍relu和sigmoid
import torch
from torch import nn
from torch.nn import ReLU, Sigmoid

input=torch.tensor([[1,-0.5],
[-1,3]])

input=torch.reshape(input,(-1,1,2,2))
print(input)
class Tudui(nn.Module):
def __init__(self):
super(Tudui, self).__init__()
self.relu1 = ReLU()
self.sigmoid1 = Sigmoid()

def forward(self, input):
output = self.relu1(input)
#output = self.sigmoid1(input)
return output

tudui = Tudui()
output=tudui(input)
print(output)


p21线性层和其它层介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

import torch
import torchvision
from torch import nn
from torch.nn import Linear
from torch.utils.data import DataLoader

dataset = torchvision.datasets.CIFAR10("../data", train=False, transform=torchvision.transforms.ToTensor(),
download=True)

dataloader = DataLoader(dataset, batch_size=64)

class Tudui(nn.Module):
def __init__(self):
super(Tudui, self).__init__()
self.linear1 = Linear(196608, 10) #输入 输出

def forward(self, input):
output = self.linear1(input) #经过线性层
return output

tudui = Tudui()

for data in dataloader:
imgs, targets = data
print(imgs.shape)
output = torch.flatten(imgs) #展开成一行
print(output.shape)
output = tudui(output)
print(output.shape)


p22实战和sequential的使用

要参考的模型图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

import torch
from torch import nn
from torch.nn import Conv2d, MaxPool2d, Flatten, Linear, Sequential
#from torch.utils.tensorboard import SummaryWriter

class Tudui(nn.Module):
def __init__(self):
super(Tudui, self).__init__()
#直接做成一个系列
self.model1 = Sequential(
Conv2d(in_channels=3, out_channels=32,kernel_size=5, padding=2), #padding=2是根据输入输出的HW公式推出来 输入3个通道 输出维度32池化核5*5
MaxPool2d(2), #默认池化层的stride步长=池化核维度
Conv2d(in_channels=32, out_channels=32,kernel_size=5, padding=2), #padding=2是根据输入输出的HW公式推出来 输入32个通道 输出维度32 池化核5*5
MaxPool2d(2), #默认池化层的stride步长=池化核维度
Conv2d(in_channels=32, out_channels=64,kernel_size=5, padding=2), #padding=2是根据输入输出的HW公式推出来 输入32个通道 输出维度64 池化核5*5
MaxPool2d(2), #默认池化层的stride步长=池化核维度
Flatten(), #展开
Linear(1024, 64), #展开之后64*4*4=1024个 一共有64个
Linear(64, 10) #展开之后64个 一共10个
)

def forward(self, x):
x = self.model1(x)
return x

@初始化看看
tudui = Tudui()
print(tudui)

#假设有一个
input = torch.ones((64, 3, 32, 32)) #相当于64张图 每张图有3个通道 32*32的
output = tudui(input)
print(output.shape)

#可视化
#writer = SummaryWriter("../logs_seq")
#writer.add_graph(tudui, input)
#writer.close()


p23损失函数(误差)和反向传播(计算误差提供可以优化的点)

两种损失函数和交叉熵

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

import torch
from torch.nn import L1Loss
from torch import nn

#输入和目标
inputs = torch.tensor([1, 2, 3], dtype=torch.float32)
targets = torch.tensor([1, 2, 5], dtype=torch.float32)
#规定想要的输入和目标的结果
inputs = torch.reshape(inputs, (1, 1, 1, 3)) #1个样本 通道为1 宽度为1*3
targets = torch.reshape(targets, (1, 1, 1, 3))

loss=L1Loss()
result=loss(inputs,targets) #tensor(0.6667)
print(result) #[(1-1)+(2-2)+(5-3)]/3

loss2=nn.MSELoss()
result2=loss2(inputs,targets) #tensor(1.3333)
print(result2) #[(1-1)+(2-2)+(5-3)^2]/3

#交叉熵
#使用在分类问题时候,有很多个类别
x=torch.tensor([0.1, 0.2, 0.3])
y=torch.tensor([1])
x=torch.reshape(x,(1, 3)) #3类
loss_cross=nn.CrossEntropyLoss()
result3=loss_cross(x,y) #tensor(1.1019)
print(result3)

带网络的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

import torchvision
from torch import nn
from torch.nn import Sequential, Conv2d, MaxPool2d, Flatten, Linear
from torch.utils.data import DataLoader

#数据集
dataset = torchvision.datasets.CIFAR10("../data", train=False, transform=torchvision.transforms.ToTensor(),
download=True)

dataloader = DataLoader(dataset, batch_size=1)

class Tudui(nn.Module):
def __init__(self):
super(Tudui, self).__init__()
#直接做成一个系列
self.model1 = Sequential(
Conv2d(in_channels=3, out_channels=32,kernel_size=5, padding=2), #padding=2是根据输入输出的HW公式推出来 输入3个通道 输出维度32池化核5*5
MaxPool2d(2), #默认池化层的stride步长=池化核维度
Conv2d(in_channels=32, out_channels=32,kernel_size=5, padding=2), #padding=2是根据输入输出的HW公式推出来 输入32个通道 输出维度32 池化核5*5
MaxPool2d(2), #默认池化层的stride步长=池化核维度
Conv2d(in_channels=32, out_channels=64,kernel_size=5, padding=2), #padding=2是根据输入输出的HW公式推出来 输入32个通道 输出维度64 池化核5*5
MaxPool2d(2), #默认池化层的stride步长=池化核维度
Flatten(), #展开
Linear(1024, 64), #展开之后64*4*4=1024个 一共有64个
Linear(64, 10) #展开之后64个 一共10个
)

def forward(self, x):
x = self.model1(x)
return x

#交叉熵
loss = nn.CrossEntropyLoss()
tudui = Tudui()
for data in dataloader:
imgs, targets = data
outputs = tudui(imgs)
result_loss = loss(outputs, targets)
print(result_loss)
print("ok")


p24优化器(让loss变小)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

import torch.optim
import torchvision
from torch import nn
from torch.nn import Sequential, Conv2d, MaxPool2d, Flatten, Linear
from torch.utils.data import DataLoader

#数据集
dataset = torchvision.datasets.CIFAR10("../data", train=False, transform=torchvision.transforms.ToTensor(),
download=True)

dataloader = DataLoader(dataset, batch_size=1)

class Tudui(nn.Module):
def __init__(self):
super(Tudui, self).__init__()
#直接做成一个系列
self.model1 = Sequential(
Conv2d(in_channels=3, out_channels=32,kernel_size=5, padding=2), #padding=2是根据输入输出的HW公式推出来 输入3个通道 输出维度32池化核5*5
MaxPool2d(2), #默认池化层的stride步长=池化核维度
Conv2d(in_channels=32, out_channels=32,kernel_size=5, padding=2), #padding=2是根据输入输出的HW公式推出来 输入32个通道 输出维度32 池化核5*5
MaxPool2d(2), #默认池化层的stride步长=池化核维度
Conv2d(in_channels=32, out_channels=64,kernel_size=5, padding=2), #padding=2是根据输入输出的HW公式推出来 输入32个通道 输出维度64 池化核5*5
MaxPool2d(2), #默认池化层的stride步长=池化核维度
Flatten(), #展开
Linear(1024, 64), #展开之后64*4*4=1024个 一共有64个
Linear(64, 10) #展开之后64个 一共10个
)

def forward(self, x):
x = self.model1(x)
return x

#交叉熵
loss = nn.CrossEntropyLoss()
tudui = Tudui()

#设置优化器
optim=torch.optim.SGD(tudui.parameters(),lr=0.01) #入门设置两个参数 模型参数和学习速率

for epoch in range(20): #套循环
wucha=0.0
for data in dataloader:
imgs, targets = data
outputs = tudui(imgs)
result_loss = loss(outputs, targets)

#增加优化器的部分
optim.zero_grad() #每次循环梯度清0
result_loss.backward() #获取每个data计算grad
optim.step() #进行优化 -->最终将loss差距变小
#计算每一轮误差优化多少
wucha=wucha+result_loss

print(result_loss)


p25现有模型

使用和修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

import torchvision
from torch import nn

vgg16_false = torchvision.models.vgg16(pretrained=False) #会下载到c盘 我的到C:\Users\larkkkkkkk\.cache\torch\hub\checkpoints
vgg16_true = torchvision.models.vgg16(pretrained=True)

print(vgg16_true)

train_data = torchvision.datasets.CIFAR10('./data', train=True, transform=torchvision.transforms.ToTensor(),
download=True)

#修改点!!!
# (6): Linear(in_features=4096, out_features=1000, bias=True)

#要么加一层线性层输出10
vgg16_true.add_module('add_Linear',nn.Linear(1000,10)) #在原来的classifier外面加
#vgg16_true.classifier.add_module('add_Linear',nn.Linear(1000,10)) #在原来的classifier里面加
print(vgg16_true)

#要么将输出层改为10
vgg16_false.classifier[6]=nn.Linear(4096,10)
print(vgg16_false)

p26保存

保存方法1(模型结构+模型参数)

1
2
3
4
5
6
7
8

import torch
import torchvision

vgg16= torchvision.models.vgg16(pretrained=False) #会下载到c盘 我的到C:\Users\larkkkkkkk\.cache\torch\hub\checkpoints

#保存
torch.save(vgg16,"vgg16_method1.pth") #模型结构+模型参数

保存方法2(模型参数)

1
2
3
4
5
6
7
8

import torch
import torchvision

vgg16= torchvision.models.vgg16(pretrained=False) #会下载到c盘 我的到C:\Users\larkkkkkkk\.cache\torch\hub\checkpoints

#保存
torch.save(vgg16.state_dict(),"vgg16_method2.pth") #模型参数 是字典形式!!!!

p26读取

读取方法1(load)

1
2
3
4
5
6

import torch
import torchvision
##读取方式1
model=torch.load("vgg16_method1.pth")
print(model)

读取方法2(考虑字典形式)

1
2
3
4
5
6
7
8
9
10
11
12

import torch
import torchvision

#读取方式2
vgg16=torchvision.models.vgg16(pretrained=False)
vgg16.load_state_dict(torch.load("vgg16_method2.pth"))
print(vgg16)
print("-----------------------------------------------------------------")
#如果不使用加载dict 那么输出就是字典形式
model2=torch.load("vgg16_method2.pth")
print(model2)


p27完整模型训练套路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

import torchvision
from torch.utils.tensorboard import SummaryWriter
from model import * # 引入model.py里的模型
from torch import nn
from torch.utils.data import DataLoader

# 1.数据集
##1.1准备数据集
train_data = torchvision.datasets.CIFAR10(root="../data", train=True, transform=torchvision.transforms.ToTensor(),
download=True)
test_data = torchvision.datasets.CIFAR10(root="../data", train=False, transform=torchvision.transforms.ToTensor(),
download=True)
##1.2查看数据集内容
train_data_size = len(train_data)
test_data_size = len(test_data)
print("训练集长度:", train_data_size) # 训练集长度: 50000
print("测试集长度:", test_data_size) # 测试集长度: 10000
##1.3加载数据集
train_dataloader = DataLoader(train_data, batch_size=64)
test_dataloader = DataLoader(test_data, batch_size=64)

# 2.创建网络模型
tudui = Tudui() # 引用model.py文件下的模型
##2.1损失函数
loss_fn = nn.CrossEntropyLoss()
##2.2优化器(反向传播时候用)
optimizer = torch.optim.SGD(tudui.parameters(), lr=0.01)

# 3.训练网络
##3.1记录训练的次数
total_train_step = 0
##3.2记录测试的次数
total_test_step = 0
##3.3训练的轮数
epoch = 10

# 添加tensorboard
writer = SummaryWriter("./logs_train")

for i in range(epoch):
print("-------第 {} 轮训练开始-------".format(i + 1))
# 训练
tudui.train() #有些特殊层需要调用(一般写上去也没事)
for data in train_dataloader:
imgs, targets = data # 图片和标签
outputs = tudui(imgs)
loss = loss_fn(outputs, targets)

# 优化器
optimizer.zero_grad() # 每一轮梯度清0
loss.backward() # 反向传播 计算出要优化的值
optimizer.step() # 优化器优化

total_train_step = total_train_step + 1
if total_train_step % 100 == 0:
print("训练次数:{}, Loss: {}".format(total_train_step, loss.item()))
writer.add_scalar("train_loss", loss.item(), total_train_step)

# 测试
tudui.eval() #有些特殊层需要调用(一般写上去也没事)
total_test_loss = 0 #整体loss
total_accuracy = 0 #整体准确率
with torch.no_grad(): #在训练之后没有梯度了才可以进循环
for data in test_dataloader:
imgs, targets = data # 图片和标签
outputs = tudui(imgs)
# 一部分数据的损失
loss = loss_fn(outputs, targets)
total_test_loss = total_test_loss + loss.item() # 每次损失增加
accuracy = (outputs.argmax(1) == targets).sum() #整体准确率 用argmax可以判断[False,True].sum()=1
total_accuracy = total_accuracy + accuracy

print("整体测试集上的Loss: {}".format(total_test_loss))
print("整体测试集上的正确率: {}".format(total_accuracy / test_data_size))
writer.add_scalar("test_loss", total_test_loss, total_test_step)
writer.add_scalar("test_accuracy", total_accuracy / test_data_size, total_test_step)
total_test_step = total_test_step + 1

torch.save(tudui, "tudui_{}.pth".format(i))
print("模型已保存")

writer.close()


p30利用GPU训练

1
2
 
在上一轮的代码中添加gpu训练

两者的区别和加入的位置:

.cuda()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112

import torchvision
from torch.utils.tensorboard import SummaryWriter
from model import * # 引入model.py里的模型
from torch import nn
from torch.utils.data import DataLoader

# 1.数据集
##1.1准备数据集
train_data = torchvision.datasets.CIFAR10(root="../data", train=True, transform=torchvision.transforms.ToTensor(),
download=True)
test_data = torchvision.datasets.CIFAR10(root="../data", train=False, transform=torchvision.transforms.ToTensor(),
download=True)
##1.2查看数据集内容
train_data_size = len(train_data)
test_data_size = len(test_data)
print("训练集长度:", train_data_size) # 训练集长度: 50000
print("测试集长度:", test_data_size) # 测试集长度: 10000
##1.3加载数据集
train_dataloader = DataLoader(train_data, batch_size=64)
test_dataloader = DataLoader(test_data, batch_size=64)

# 2.创建网络模型
##2.1搭建神经网络
class Tudui(nn.Module):
def __init__(self):
super(Tudui, self).__init__()
self.model = nn.Sequential(
nn.Conv2d(3, 32, 5, 1, 2),
nn.MaxPool2d(2),
nn.Conv2d(32, 32, 5, 1, 2),
nn.MaxPool2d(2),
nn.Conv2d(32, 64, 5, 1, 2),
nn.MaxPool2d(2),
nn.Flatten(),
nn.Linear(64 * 4 * 4, 64),
nn.Linear(64, 10)
)

def forward(self, x):
x = self.model(x)
return x

tudui = Tudui() # 引用model.py文件下的模型
tudui=tudui.cuda()

##2.2损失函数
loss_fn = nn.CrossEntropyLoss()
loss_fn=loss_fn.cuda()

##2.3优化器(反向传播时候用)
optimizer = torch.optim.SGD(tudui.parameters(), lr=0.01)


# 3.训练网络
##3.1记录训练的次数
total_train_step = 0
##3.2记录测试的次数
total_test_step = 0
##3.3训练的轮数
epoch = 10

# 添加tensorboard
writer = SummaryWriter("./logs_train")

for i in range(epoch):
print("-------第 {} 轮训练开始-------".format(i + 1))
# 训练
tudui.train() #有些特殊层需要调用(一般写上去也没事)
for data in train_dataloader:
imgs, targets = data # 图片和标签
imgs=imgs.cuda()
targets=targets.cuda()
outputs = tudui(imgs)
loss = loss_fn(outputs, targets)

# 优化器
optimizer.zero_grad() # 每一轮梯度清0
loss.backward() # 反向传播 计算出要优化的值
optimizer.step() # 优化器优化

total_train_step = total_train_step + 1
if total_train_step % 100 == 0:
print("训练次数:{}, Loss: {}".format(total_train_step, loss.item()))
writer.add_scalar("train_loss", loss.item(), total_train_step)

# 测试
tudui.eval() #有些特殊层需要调用(一般写上去也没事)
total_test_loss = 0 #整体loss
total_accuracy = 0 #整体准确率
with torch.no_grad():
for data in test_dataloader:
imgs, targets = data # 图片和标签
imgs = imgs.cuda()
targets = targets.cuda()
outputs = tudui(imgs)
# 一部分数据的损失
loss = loss_fn(outputs, targets)
total_test_loss = total_test_loss + loss.item() # 每次损失增加
accuracy = (outputs.argmax(1) == targets).sum() #整体准确率 用argmax可以判断[False,True].sum()=1
total_accuracy = total_accuracy + accuracy

print("整体测试集上的Loss: {}".format(total_test_loss))
print("整体测试集上的正确率: {}".format(total_accuracy / test_data_size))
writer.add_scalar("test_loss", total_test_loss, total_test_step)
writer.add_scalar("test_accuracy", total_accuracy / test_data_size, total_test_step)
total_test_step = total_test_step + 1

torch.save(tudui, "tudui_{}.pth".format(i))
print("模型已保存")

writer.close()

更改位置:

.torch.device(“cuda:0”)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121

import torchvision
from torch.utils.tensorboard import SummaryWriter
from model import * # 引入model.py里的模型
from torch import nn
from torch.utils.data import DataLoader

#设置GPU
device=torch.device("cuda:0") #要考虑有几张卡

# 1.数据集
##1.1准备数据集
train_data = torchvision.datasets.CIFAR10(root="../data", train=True, transform=torchvision.transforms.ToTensor(),
download=True)
test_data = torchvision.datasets.CIFAR10(root="../data", train=False, transform=torchvision.transforms.ToTensor(),
download=True)
##1.2查看数据集内容
train_data_size = len(train_data)
test_data_size = len(test_data)
print("训练集长度:", train_data_size) # 训练集长度: 50000
print("测试集长度:", test_data_size) # 测试集长度: 10000
##1.3加载数据集
train_dataloader = DataLoader(train_data, batch_size=64)
test_dataloader = DataLoader(test_data, batch_size=64)

# 2.创建网络模型
##2.1搭建神经网络
class Tudui(nn.Module):
def __init__(self):
super(Tudui, self).__init__()
self.model = nn.Sequential(
nn.Conv2d(3, 32, 5, 1, 2),
nn.MaxPool2d(2),
nn.Conv2d(32, 32, 5, 1, 2),
nn.MaxPool2d(2),
nn.Conv2d(32, 64, 5, 1, 2),
nn.MaxPool2d(2),
nn.Flatten(),
nn.Linear(64 * 4 * 4, 64),
nn.Linear(64, 10)
)

def forward(self, x):
x = self.model(x)
return x

tudui = Tudui() # 引用model.py文件下的模型
#tudui=tudui.cuda()
tudui=tudui.to(device)

##2.2损失函数
loss_fn = nn.CrossEntropyLoss()
#loss_fn=loss_fn.cuda()
loss_fn=loss_fn.to(device)

##2.3优化器(反向传播时候用)
optimizer = torch.optim.SGD(tudui.parameters(), lr=0.01)


# 3.训练网络
##3.1记录训练的次数
total_train_step = 0
##3.2记录测试的次数
total_test_step = 0
##3.3训练的轮数
epoch = 10

# 添加tensorboard
writer = SummaryWriter("./logs_train")

for i in range(epoch):
print("-------第 {} 轮训练开始-------".format(i + 1))
# 训练
tudui.train() #有些特殊层需要调用(一般写上去也没事)
for data in train_dataloader:
imgs, targets = data # 图片和标签
#imgs=imgs.cuda()
#targets=targets.cuda()
imgs=imgs.to(device)
targets=targets.to(device)
outputs = tudui(imgs)
loss = loss_fn(outputs, targets)

# 优化器
optimizer.zero_grad() # 每一轮梯度清0
loss.backward() # 反向传播 计算出要优化的值
optimizer.step() # 优化器优化

total_train_step = total_train_step + 1
if total_train_step % 100 == 0:
print("训练次数:{}, Loss: {}".format(total_train_step, loss.item()))
writer.add_scalar("train_loss", loss.item(), total_train_step)

# 测试
tudui.eval() #有些特殊层需要调用(一般写上去也没事)
total_test_loss = 0 #整体loss
total_accuracy = 0 #整体准确率
with torch.no_grad():
for data in test_dataloader:
imgs, targets = data # 图片和标签
# imgs=imgs.cuda()
# targets=targets.cuda()
imgs = imgs.to(device)
targets = targets.to(device)
outputs = tudui(imgs)
# 一部分数据的损失
loss = loss_fn(outputs, targets)
total_test_loss = total_test_loss + loss.item() # 每次损失增加
accuracy = (outputs.argmax(1) == targets).sum() #整体准确率 用argmax可以判断[False,True].sum()=1
total_accuracy = total_accuracy + accuracy

print("整体测试集上的Loss: {}".format(total_test_loss))
print("整体测试集上的正确率: {}".format(total_accuracy / test_data_size))
writer.add_scalar("test_loss", total_test_loss, total_test_step)
writer.add_scalar("test_accuracy", total_accuracy / test_data_size, total_test_step)
total_test_step = total_test_step + 1

torch.save(tudui, "tudui_{}.pth".format(i))
print("模型已保存")

writer.close()

更改位置:


p32完整模型验证套路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import torch
import torchvision
from PIL import Image
from torch import nn

#读取图片
image_path = "./imgs/dog.png"
image = Image.open(image_path)
print(image) #<PIL.PngImagePlugin.PngImageFile image mode=RGB size=1053x825 at 0x26FB60DF880>


#png格式是四个通道,除了RGB以外还有一个透明度通道
image=image.convert('RGB')#保留其颜色通道

transform = torchvision.transforms.Compose([torchvision.transforms.Resize((32, 32)), #网络输入是32*32维
torchvision.transforms.ToTensor()])
image=transform(image)
print(image.shape) #torch.Size([3, 32, 32])

#模型
class Tudui(nn.Module):
def __init__(self):
super(Tudui, self).__init__()
self.model = nn.Sequential(
nn.Conv2d(3, 32, 5, 1, 2),
nn.MaxPool2d(2),
nn.Conv2d(32, 32, 5, 1, 2),
nn.MaxPool2d(2),
nn.Conv2d(32, 64, 5, 1, 2),
nn.MaxPool2d(2),
nn.Flatten(),
nn.Linear(64*4*4, 64),
nn.Linear(64, 10)
)

def forward(self, x):
x = self.model(x)
return x

model = torch.load("tudui_29_gpu.pth", map_location=torch.device('cpu'))
#print(model)

#开始验证
#输入时候记得设置batch_size
image = torch.reshape(image, (1, 3, 32, 32))
model.eval() #有些特殊层需要

with torch.no_grad():
output = model(image)
print("预测结果是:",output)

print("最大的可能是:",output.argmax(1))


python打包

python打包

1
2
3

1.pyinstaller
2.

pyinstaller使用

参考博客

https://blog.csdn.net/weixin_40829804/article/details/129412968?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-129412968-blog-105585426.235%5Ev36%5Epc_relevant_default_base3&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-129412968-blog-105585426.235%5Ev36%5Epc_relevant_default_base3&utm_relevant_index=2

pyinstaller安装

1
2

pip install pyinstaller (在conda环境/pycharm进入之后找到解释器位置搜索安装也行)

pyinstaller遇到的问题

1
2

1.conda自带的pathlib包和pyinstaller冲突 -->直接 conda remove pathlib

打包单个文件步骤

  • 1.进入代码路径 –>打开终端输入 pyinstaller -F -w API.py

  • 2.更新出一个dist文件夹,打开之后出现一个exe文件

打包整个项目步骤

  • 1.进入代码路径 –>打开终端输入 pyi-makespec 要执行的.py

  • 2.打开app.spec文件,进行参数修改,主要修改a = Analysis的模块即可,说明如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

a = Analysis(
['app.py','app_rating.py','config.py','exts.py','models.py'],
#要打包的.py文件列表,同级文件不需要添加
pathex=['D:\Project Management System\Program\beta1.2_Problems'],
#项目路径
binaries=[],
datas=[('./templates','templates'),('./static','static')],
# 程序调用外部pyd、dll文件(二进制文件路径)以数组形式传入,数组第一个参数传本地文件的绝对路径,第二参数传打包文件夹所在路径,打点为根目录;
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)

  • 3.终端执行 pyinstaller 要执行的.spec

  • 4.在与要执行的.py相同的文件路径下,找到dist文件夹,即可找到打包好的app.exe

pytorch

pytorch汇总

pytorch加载数据初认识

数据集

1
下载位置:https://download.pytorch.org/tutorial/hymenoptera_data.zip

构建

1
2


##

#

django

介绍

基本介绍

1
2
3
4
5

1.Django是python下的Web框架,许多网站和APP都基于它,
2.Django是一个开放源代码的Web应用框架,遵守BSD版本,
3.Django可以通过很少的代码完成一个正式网站需要的大部分内容,
4.Django基于MVC模式,可以方便对程序的修改和扩展简化,并且使程序某一部分的重复利用成为可能,

特点

1
2
3
4
5

1.Django是python下的Web框架,许多网站和APP都基于它,
2.Django是一个开放源代码的Web应用框架,遵守BSD版本,
3.Django可以通过很少的代码完成一个正式网站需要的大部分内容,
4.Django基于MVC模式,可以方便对程序的修改和扩展简化,并且使程序某一部分的重复利用成为可能,

MVC模型

MTV模型

安装和创建第一个项目

安装

1
2
3
4
1.参考文件:https://www.runoob.com/django/django-install.html
2.步骤:
2.1 安装python:记得配置path
2.2 安装django:使用pip install Django

创建(pycharm)

文件格式

代码结构:

创建项目步骤

1
2

#项目名称假如是:mysite

flask接口

介绍

1
2

Falsk是由python开发的轻量的web框架,小巧,灵活,一个脚本就可以启动一个web项目, 开发的难度比较大,flask好多的模块是按照django的思路开发的。和flask类似的框架还有web.py,

具体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

from flask import Flask,request,jsonify,make_response,redirect

def result(value,txt):
user=" "
print("传进来结果是:",value)
print("传进来要判断的功能是:",txt)
if value=='1': #抽取特定json格式
print("进入了:",value)
res=caozuo1(txt['text']) #根据前端给的报文的格式要用txt['text']
print("结果是:", res)
user=res #这里可以将res的结果拆分开给user 做一个{}
elif value=='2': #抽取雷达
print("进入了:", value)
res=caozuo2(txt['text'])
print("结果是:", res)
user=res #这里可以将res的结果拆分开给user 做一个{}
elif value=='3': #抽取高亮的营地
print("进入了:", value)
res=caozuo3(txt['text'])
print("结果是:",res)
user=res #这里可以将res的结果拆分开给user 做一个{}
return user

#创建Flask应用程序实例
app=Flask(__name__)

app.config.from_object(__name__)
app.config["JSON_AS_ASCII"] = False #返回中文时候要考虑设定为False

#定义一个路由来处理请求,接受json数据
#第一个参数 地址之后要加的
#第二个参数 访问的get/post请求
@app.route("/json",methods=['GET','POST']) #局域网内其他电脑可以通过IP:5000/json访问和传入参数
def edit_user():
response=jsonify(result(request.form.todict()['type'],request.form.to_dict())) #对应进行序列化
#解决前后端跨域问题!(不添加会导致可以接受但是结果无法返回到前端json里面)
response.headers.add('Access-Control-Allow-Origin','*')
response.headers.add('Access-Control-Allow-Methods','PUT,GET,POST,DELETE')
return response

#配置host才可以让局域网内的其他电脑访问!!!!!!!!!!!!!!!!! 一个大bug!!!!!!
app.run(host='0.0.0.0',port=5000,debug=True)

具体截图如下:


,