简介
Java学习笔记
Java语言详解
001. JDK安装与配置
JRE与JDK
- JRE(Java 运行环境):包含了 Java 虚拟机(JVM)和类库,用于运行 Java 应用程序。
- JDK(Java 开发工具包):在 JRE 的基础上增加了一些开发工具,主要用于开发 Java 应用程序。
下载并安装JDK8
运行安装程序,按照提示进行安装,建议使用默认路径
配置环境变量
增加 JAVA_HOME 环境变量,指向 JDK 安装的根目录。
编辑 Path 环境变量,增加 %JAVA_HOME%路径,确保可以在命令行中直接使用 Java 和相关工具。
002. Java语言特点
面向对象、强类型、垃圾自动回收:
面向对象:以对象为核心组织代码,通过对象和类的概念来描述和解决实际问题。
封装(Encapsulation): 将数据(属性)和操作(方法)封装在对象中,限制对内部状态的直接访问,通过公开的接口与外界交互。
继承(Inheritance): 子类可以继承父类的属性和方法,支持代码复用和扩展。
多态(Polymorphism): 对象可以以不同的形式出现,具体表现为方法的重载(Overloading)和重写(Overriding)。
强类型(Strong Typing):
对变量类型有严格要求,类型不兼容的变量或对象之间不能进行隐式转换,或需要显式转换才可操作。
垃圾自动回收(Garbage Collection, GC):
垃圾自动回收是一种自动管理内存的机制,用来回收不再使用的对象或变量占用的内存空间,避免内存泄漏和手动释放内存的复杂操作。
工作原理: 垃圾回收器通过跟踪对象的引用关系,确定哪些对象是“可达的”,哪些是“不可达的”。 不可达的对象被视为“垃圾”,由垃圾回收器回收。
优点:
- 自动管理内存,降低开发复杂度。
- 减少内存泄漏和悬挂指针的问题。
缺点:
- 增加了运行时开销,可能引发“停顿”(Stop-The-World)。
- 对实时性要求较高的应用可能需要手动优化。
概念 定义 优点 示例语言 面向对象 以对象为核心,通过封装、继承、多态等特性组织代码 提高代码复用性、模块化和扩展性 Java、C++、Python 强类型 严格要求变量类型,类型不匹配需显式转换 提高程序安全性,减少运行时错误 Java、C#、Go 垃圾自动回收 自动管理内存,回收不再使用的对象占用的空间 简化内存管理,降低内存泄漏风险 Java、Python、C# 解释型
- 解释型语言包括:JavaScript、Java、Python、PHP。
- 编译型语言包括:C/C++。
跨平台
- 提供了一个 Java 示例程序:
public class Demo { public static void main(String[] args) { System.out.println("Hello China!"); } }
- 编译命令:
javac Demo.java
- 运行命令:
java Demo
- 解释了 Java 的跨平台特性:编译后生成的
.class
文件可以在不同平台(Windows/Linux/Mac)上运行,因为 Java 虚拟机(JVM)会处理底层平台的差异,实现了“一次编译,处处运行”。
- 提供了一个 Java 示例程序:
003. 第一个Java程序
- 第一个程序
- 使用 Notepad++ 进行安装和编写代码。
- Java 示例代码:
public class Hello {
public static void main(String[] args) {
System.out.println("Hello China!");
}
}
Java 程序的注意事项
Java 源文件扩展名:以
.java
作为扩展名。区分大小写:Java 语言是区分大小写的。
public class:
public class Hello
表示Hello
是一个公共类,一个源文件只能有一个公共类,并且公共类的类名必须与文件名一致,其他类的数量不限。如果一个文件含有多个类在使用javac编译得到的会是多个.class文件,每一个文件名对应类名。大括号的使用:代码块以
{}
表示开始和结束。main 方法:
main
方法是程序的入口,必须有,且格式不能更改。输出:示例程序中的
System.out.println("Hello China!");
用于打印输出。语句结束符:每个语句以分号
;
结尾。符号使用:所有符号都必须使用英文状态下的符号。
编码:源文件使用 UTF-8 编码格式。
004. Java语言的基础组成
- 注释:代码中的注释,用于解释或标注,编译时不会执行。
- 变量:存储数据的容器,可以在程序中被修改。
- 常量:与变量类似,但其值一旦设置就不能再改变。
- 关键字、保留字:Java
中的保留字或关键字,用于定义程序的结构,如
if
、for
、class
等。 - 标识符:用于命名变量、类、方法等的符号。
- 运算符:用于执行操作的符号,如
+
、-
、*
、/
。 - 语句:Java 程序中的基本执行单位,通常以分号
;
结尾。 - 函数:又称方法,封装了一段代码逻辑,可以被调用执行。
- 数组:存储相同类型数据的集合。
Java的一些高级概念:
- 类:Java 中的基本结构,用于创建对象。
- 集合:存储多个对象的框架,如
ArrayList
、HashSet
等。 - 反射:Java 中的一个机制,允许在运行时查看和修改类的结构。
- 加密库:用于处理数据加密和解密。
- 流:用于输入输出操作的机制。
- Socket:网络通信中的一种方式。
- 泛型:允许在类和方法中使用类型参数。
- 多线程:允许程序同时执行多个线程,提升性能。
- 正则表达式:用于文本匹配和处理的模式。
重点是集合,反射和加密库,这三部分是重中之重。
005. 注释
单行注释
- 格式为
// 注释文字
- 用于在一行内添加注释内容。
- 格式为
多行注释
- 格式为
/* 注释文字 */
- 用于跨多行的注释内容。
- 格式为
注释的作用
- 被注释的文字不会被 JVM 解释执行,注释内容仅供开发者阅读,不会影响程序运行。
- 多行注释不能嵌套使用。
编码问题导致编译失败
如果由于编码问题导致编译失败,可以通过设置环境变量
JAVA_TOOL_OPTIONS
来解决。设置方式为:
-Dfile.encoding=UTF-8
,用于确保 Java 使用 UTF-8 编码格式。
终端中使用javac编译含有中文注释的.java文件会报错,因为文件使用UTF-8 编码,Windows默认使用GBK编码,可以将文件设置为ansi编码,或者像上面那样使用环境变量。
006. IDEA的安装与配置
IDEA 的安装
- 下载地址: https://www.jetbrains.com/idea/
- 获取白嫖版本的方法:可以使用社区版、试用版、通过百度/淘宝购买或者使用 edu 邮箱获取学生优惠。
- 系统要求:
- 内存:至少 8GB
- CPU:建议 i5 及以上
- 最好安装在 SSD 上以提升性能。
IDEA 的配置
- 通过修改
idea64.exe.vmoptions
文件,可以设置:- 初始内存
- 最大内存
- 缓存大小
- 另外可以配置:
- 创建工程
- 调出工具栏
- 配置字体大小等 UI 选项。
- 通过修改
配置文件的默认位置与修改
- 旧版本的默认路径为:
C:\Users\Administrator
- 新版本的默认路径为:
C:\Users\Administrator\AppData\Roaming\JetBrains
- 如果需要重置配置,只需删除配置文件,IDEA 会在下次启动时重新生成默认配置。
- 旧版本的默认路径为:
007. 转义字符
\t
- 制表符,用于插入一个水平制表符(Tab)。\n
- 换行符,用于插入一个换行符(回车换行)。\\
- 表示一个反斜杠\
。\"
- 表示一个双引号"
, 用于在字符串中插入双引号。\'
- 表示一个单引号'
, 用于在字符串中插入单引号。
public class Hello {
@SuppressWarnings("all")
public static void main(String[] args) {
System.out.println("Hello\tJava\nIDEA");
System.out.println("Hello\\Java");
}
}
代码中的转义字符: 1. \t
-
插入一个制表符(Tab),因此输出时 "Hello" 和 "Java"
之间会有一个水平制表符的空格。 2. \n
- 插入一个换行符,因此
"Java" 和 "IDEA" 会在不同的行显示。 3. \\
-
插入一个反斜杠,输出时会显示为一个反斜杠。
输出结果如下:
Hello Java
IDEA
Hello\Java
第一行的 Hello\tJava\nIDEA
表示 "Hello" 和 "Java"
之间有制表符,而 "Java" 和 "IDEA" 之间有换行符。第二行的
Hello\\Java
表示反斜杠的转义,输出时会显示为 "Hello"。
@SuppressWarnings("all")
是 Java
中的一个注解,作用是抑制编译器在编译时生成的所有警告。具体解释如下:
@SuppressWarnings
:这是 Java 提供的一个注解,用于告诉编译器在指定的代码范围内忽略某些类型的警告。"all"
:这是注解的参数,表示抑制所有类型的警告。Java 编译器通常会发出不同类型的警告,如未使用的变量、过时的方法、泛型类型安全等。而使用"all"
参数意味着这些警告将不会在编译时提示。
在这段代码中,@SuppressWarnings("all")
放在
main
方法上方,因此该方法内的所有潜在的警告都将被忽略。
- 过度使用
@SuppressWarnings("all")
会使你忽略掉可能的代码问题。因此,最好针对特定的警告进行抑制,而不是直接忽略所有警告。 - 常见的可抑制警告类型还包括
"unchecked"
(抑制泛型的未检查警告)、"deprecation"
(抑制调用过时方法的警告)等。
008. Java(变量)
- 为什么需要变量,变量的作用?
- 变量用于存储和表示数据,方便程序运行时进行数据的操作和处理。
- 变量的概念
- 变量其实就是内存中的一块存储区域,声明变量就是在内存中开辟空间,用来存储数据。变量的声明需要包含数据的类型(数据类型)和一个标识该变量的名字(变量名)。
- 定义变量的格式
- 变量定义的一般格式为:
数据类型 变量名 = 初始值;
- 变量在定义后通常会进行初始化。
- 变量需要先声明,后使用。
- 同一作用域内,变量名不能重复。
- 变量可以在同一类型范围内不断变化。
- 变量定义的一般格式为:
- 变量三要素
- 类型、名称、值。
- 标识符
- 标识符区分大小写。
- 由字母和数字组成,且不能以数字开头,不能使用关键字。
- 驼峰命名法或下划线命名法可以用于定义标识符。
public class Hello {
@SuppressWarnings("all")
public static void main(String[] args) {
int a = 0;
System.out.println(a); // 输出 0
a = 100;
// byte a = 1; // 该行被注释掉,重新声明a为byte类型是不允许的
System.out.println(a); // 输出 100
System.out.println("Hello\tJava\nIDEA");
System.out.println("Hello\\Java");
System.out.println("Hello\"Ja\"va");
// System.out.println('Hello'); // 该行被注释掉,单引号内只能包含一个字符
}
}
代码解释:
变量声明和赋值
- 首先声明了一个
int
类型的变量a
,并将其初始化为 0,接着输出a
的值。 - 然后将
a
重新赋值为 100,并再次输出a
的值。
- 首先声明了一个
注释的
byte a = 1;
- 这行代码被注释掉,因为在 Java
中不能在同一个作用域内重复声明同名变量。并且
a
已经被声明为int
类型,无法重新声明为byte
类型。
- 这行代码被注释掉,因为在 Java
中不能在同一个作用域内重复声明同名变量。并且
转义字符的使用
System.out.println("Hello\tJava\nIDEA");
:这里使用了\t
(制表符)和\n
(换行符),输出结果会是:Hello Java IDEA
System.out.println("Hello\\Java");
:这里使用了\\
来插入一个反斜杠,输出结果是:Hello\Java
System.out.println("Hello\"Ja\"va");
:这里使用了\"
来插入双引号,输出结果是:Hello"Ja"va
注释掉的单引号
System.out.println('Hello');
被注释掉是因为在 Java 中,单引号只能用于字符(char
)类型,且只能包含一个字符。因此,'Hello'
是不合法的,应使用双引号来表示字符串。
009. Java(数据类型)
Java 中的数据类型分类,分为八大基本数据类型和引用数据类型。具体内容如下:
1. 八大基本数据类型
数值型:
- 整数类型:
byte
、short
、int
、long
- 浮点类型:
float
、double
字符型:
char
:char
类型占用 2 个字节,本质上是一个数值。和C语言不同,C语言占一个字节。
布尔型:
boolean
:表示布尔值,取值为true
或false
。
2. 引用数据类型
- 类 (
class
) - 接口 (
interface
) - 数组 (
[]
)
在 Java 中,基本数据类型用于存储简单的值,而引用数据类型用于存储对象的引用。
字符串是引用数据类型
在 Java 中,字符串是引用数据类型。字符串由
String
类表示,因此任何字符串数据在 Java 中实际上是
String
类的对象。例如:
String str = "Hello, World!";
String
类在 Java 的java.lang
包中。- 虽然字符串是用双引号表示的字面值,但它们不是基本数据类型,而是
String
类的实例。 - Java
的字符串是不可变的,也就是说,一旦创建,字符串的内容不能被修改。如果对字符串进行修改操作,实际上会生成一个新的
String
对象。
特点:
- 不可变:一旦字符串对象被创建,它的值不能被更改。
- 字符串池:Java 会维护一个字符串池,如果多个字符串具有相同的值,Java 不会为每个字符串创建新的对象,而是重用池中的对象。
- 常用方法:
String
类提供了很多操作字符串的方法,比如length()
、substring()
、toUpperCase()
等。
因此,Java 中的字符串是引用类型,属于 String
类。
010. Java(整数类型)
1. 整数类型的定义
- 整数类型是用于存放整数值的,如
100
、200
、300
等。
2. 四种整数类型
- byte(字节):1 字节,范围为 -128 到 127。
- short(短整型):2 字节。
- int(整型):4 字节,是 Java 中的默认整数类型。
- long(长整型):8 字节。
3. 整数在代码中的默认类型
- 出现在代码中的整数默认是
int
类型。 - 如果要声明
long
类型的常量,需在数字后面加L
或l
。
示例代码:
int a = 1; // 这是合法的,1 是 int 类型。
int b = 1L; // 编译错误,1L 是 long 类型,不能直接赋值给 int。
long c = 1L; // 这是合法的,1L 是 long 类型。
在这个示例中:
int a = 1;
合法,因为1
默认是int
类型。int b = 1L;
会出错,因为1L
是long
类型,不能赋值给int
。long c = 1L;
合法,表示一个长整型常量赋值给long
变量。
011. Java(浮点类型)
1. 浮点类型的定义
- 浮点类型可以表示一个小数,例如 0.13、1.0、.43 等。
2. 浮点类型分类
- float:4 字节
- double:8 字节
3.
出现在代码中的小数,默认是 double
类型
- 声明
float
类型的常量时,必须在数值后加F
或f
。
示例代码:
float a = 10.1; // 错误,10.1 默认是 double,不能直接赋值给 float
float b = 10.1F; // 合法,10.1F 是 float 类型
double c = 10.1; // 合法,10.1 默认是 double 类型
double d = 10.1F; // 合法,float 类型可以赋值给 double 类型
4. 科学计数法
- 可以使用科学计数法表示浮点数,例如
4.3e3
(表示 4.3 * 10^3)和4.3e-3
(表示 4.3 * 10^-3)。
5. 小数精度问题
- 例如,
1.1
可能与3.3/3
在某些情况下不会完全相等,这是由于浮点数在计算机中存储的精度限制。
012. Java(字符型)
1. 字符类型的介绍
- 字符类型用于存储单个字符,Java 的
char
占用 2 个字节,可以存储汉字等 Unicode 字符。
2. 测试案例
char c1 = 'a';
char c2 = '\t';
char c3 = '中';
char c4 = 97;
char
的本质是一个整数,输出的是 Unicode 码对应的字符。如果想要输出对应的整数值,需要强制转换(int) c4
。
3. 字符型的存储
- 存储:字符
'a'
会存储为整数 97,并以二进制形式保存。 - 读取:二进制 97 会被转换回字符
'a'
。
4. char
类型可以进行运算
char a = 'a' + 1;
System.out.println(a);
System.out.println((int) a);
- 在这段代码中,字符
'a'
的 ASCII 值是 97,'a' + 1
的结果是 98,对应的字符是'b'
。 - 输出时,
System.out.println(a);
会输出字符'b'
,而System.out.println((int) a);
会输出整数 98。
013. Java(布尔型)
1. 布尔类型
- 布尔类型占用一个字节,取值为
true
或false
。在 Java 中不能用 0 或非 0 数据代替false
和true
。
2. 自动类型转换
- Java 在进行赋值或运算时,精度较小的类型可以自动转换为精度较大的类型。这种转换不需要手动干预,称为自动类型转换。
3. 数据类型按精度大小排序
- char -> int -> long -> float -> double
- byte -> short -> int -> long -> float -> double
这意味着在计算过程中,低精度的数据类型会自动转换为高精度的数据类型。例如,int
可以自动转换为 long
,float
可以自动转换为
double
。
4. 自动转换的注意事项
- boolean 类型不参与自动类型转换:布尔类型不能转换为其他类型,也不能与其他类型混合运算。
byte
和char
、short
之间不会自动转换:即使这些类型占用字节数相同或接近,它们之间不会自动转换。- 当混合多种数据类型进行运算时,数据会自动转换为精度最高的类型再进行运算。
- 在进行
byte
、short
和char
的计算时,它们会自动转换为int
类型,然后再进行运算。
014. Java(自动类型转换)
1. 布尔类型
- 布尔类型占用一个字节,取值为
true
或false
。在 Java 中不能用 0 或非 0 数据代替false
和true
。
2. 自动类型转换
- Java 在进行赋值或运算时,精度较小的类型可以自动转换为精度较大的类型。这种转换不需要手动干预,称为自动类型转换。
3. 数据类型按精度大小排序
- char -> int -> long -> float -> double
- byte -> short -> int -> long -> float -> double
在表达式计算中或操作符参与时发生的类型提升,而不适用于直接赋值的场景。在赋值操作中,Java 要求类型必须完全匹配或显式指定转换。
Java中浮点数默认是double,除非你在数字后面加F。
这意味着在计算过程中,低精度的数据类型会自动转换为高精度的数据类型。例如,int
可以自动转换为 long
,float
可以自动转换为
double
。
4. 自动转换的注意事项
- boolean 类型不参与自动类型转换:布尔类型不能转换为其他类型,也不能与其他类型混合运算。
byte
和char
、short
之间不会自动转换:即使这些类型占用字节数相同或接近,它们之间不会自动转换。- 当混合多种数据类型进行运算时,数据会自动转换为精度最高的类型再进行运算。
- 在进行
byte
、short
和char
的计算时,它们会自动转换为int
类型,然后再进行运算。 比如说:short h = e + f
如果e和f分别是short和byte编译会报错,int到short可能会有损失。
5. 测试案例
示例1:
int a = 1;
int a = 'c'; // 错误:变量名 a 已经声明,不能重复声明
- 错误在于重复声明了变量
a
。同一个作用域中不能有两个相同名字的变量。
示例2:
int a = 1;
float b = a + 0.1; // 自动转换为 float 类型
a
是int
,0.1
是double
,因此表达式a + 0.1
的结果是double
,然后赋值给float
变量b
,这会产生类型转换时的精度丢失。
示例3:
int a = 1;
float c = a + 0.1F; // 正确:a + 0.1F 的结果是 float
- 这里
0.1F
是float
类型,所以表达式a + 0.1F
的结果也是float
,可以直接赋值给float
类型的变量c
。
示例4:
int e = 100;
byte f = e; // 错误:int 不能自动转换为 byte,需要强制类型转换
int
类型的值不能自动转换为byte
,因为byte
的范围较小,必须进行显式的强制类型转换。
示例5:
byte d = 127; // 正确:127 在 byte 的范围内
127
在byte
类型的范围内(-128 到 127),所以这是合法的。
示例6:
byte d = 127;
char g = d; // 错误:byte 不能自动转换为 char,需要强制类型转换
byte
和char
是不同的类型,不能自动转换,需要显式的强制转换。
示例7:
byte h = 1;
byte i = 2;
byte j = h + i; // 错误:加法运算会将 byte 自动提升为 int,需要强制类型转换
- 在 Java 中,算术运算会将
byte
、short
和char
自动提升为int
类型,因此h + i
的结果是int
,需要强制转换为byte
。
示例8:
byte a = 2;
a = a + 3; // 错误:同上,加法运算自动提升为 int,需要强制转换
- 同样的,
a + 3
的结果是int
,不能直接赋值给byte
,需要显式转换。
示例9:
byte a = 2;
int b = 0;
b = a + 3; // 正确:a + 3 的结果是 int,可以赋值给 int 类型的 b
- 这是合法的,
a + 3
自动提升为int
,结果可以直接赋值给int
类型的b
。
示例10:
byte b1 = 3;
byte b2 = 7;
int x = b1 + b2; // 正确:b1 + b2 的结果是 int,可以赋值给 int
- 这是合法的,
byte
加法结果自动提升为int
,并赋值给int
类型的变量x
。
示例11:
short a = 100;
int b = 200;
short c = a + b; // 错误:a + b 的结果是 int,不能赋值给 short
a + b
的结果是int
,不能直接赋值给short
,需要显式转换。
示例12:
boolean a = true;
int c = a + 1; // 错误:boolean 不能参与算术运算
boolean
不能与整数相加,也不能转换为int
。这会导致编译错误。
6. 其他类型自动转换的案例
示例1:
int a = 100;
System.out.println(a + "1"); // 输出:1001
a
是int
,"1"
是字符串,+
操作符会把a
自动转换为字符串并进行字符串拼接,输出结果是1001
。
示例2:
boolean a = true;
System.out.println(a + "1"); // 输出:true1
- 同样的,
a
是boolean
,"1"
是字符串,+
操作符会把boolean
转换为字符串并进行拼接,输出结果是true1
。
赋值操作自动类型转换四种情况
1. byte
不能自动转换为 char
—— 正确
- 原因:
byte
是一个有符号的 8 位整数,取值范围为 -128 到 127;而char
是无符号的 16 位字符类型,取值范围为 0 到 65535。由于这两者的范围和表示的数据类型不同(byte
用于整数,char
用于 Unicode 字符),它们之间不能自动转换。如果byte
的值为负数,自动转换可能会导致数据损失或错误,因此 Java 不允许自动转换,必须显式进行强制类型转换。
byte b = 10;
char c = (char) b; // 需要强制转换
2. byte
不能自动转换为 short
—— 正确
解释:
- Java
允许自动拓宽转换,即可以将较小范围的类型自动转换为较大范围的类型,而不需要显式的强制类型转换。由于
short
的取值范围 (-32,768 到 32,767) 包含了byte
的取值范围 (-128 到 127),所以byte
可以自动转换为short
。 - 但是Java 的隐式转换规则适用于表达式中的类型提升,而不是赋值操作。在赋值操作中,Java 要求类型必须完全匹配或显式指定转换。
示例:
byte b = 10;
short s = b; // 编译错误,因为 byte 和 short 是不同类型
3. char
不能自动转换为 short
—— 正确
- 原因:
char
是无符号的 16 位类型,取值范围为 0 到 65535,而short
是有符号的 16 位类型,取值范围为 -32768 到 32767。因为char
的值总是非负的,而short
可以为负数,自动转换可能导致数据的歧义。因此,Java 不允许char
自动转换为short
,必须通过强制类型转换。
char c = 'A';
short s = (short) c; // 需要强制转换
4. short
不能自动转换为 char
—— 正确
- 原因:同样的原因,
short
是有符号类型,可以表示负数,而char
是无符号的,只能表示非负数。如果short
包含负值,自动转换为char
可能会导致数据错误。为了避免这种情况,Java 不允许short
自动转换为char
,必须显式转换。
short s = 100;
char c = (char) s; // 需要强制转换
总结:
byte
不能自动转换为char
—— 正确byte
不能自动转换为short
—— 正确char
不能自动转换为short
—— 正确short
不能自动转换为char
—— 正确
015. Java(强制类型转换)
1. 强制类型转换的需求
- 当类型不一致,且不符合自动转换的要求,但你仍需要进行转换时,就需要使用强制类型转换。强制转换可以将大范围的数据类型转换为小范围的数据类型,但需要注意数据的可能丢失或精度损失。
2. 强制类型转换的优先级
- 强制转换的优先级较高,需要明确指定要转换的是表达式的哪一部分。例如,将一个
double
类型的数值强制转换为int
时,必须在该数值前加上(int)
来表示这是强制转换。
3. 类型转换不仅适用于基本数据类型
- 强制类型转换不仅存在于基本数据类型,引用数据类型之间也可以进行强制类型转换(例如父类与子类之间的转换)。但这张图的示例主要是关于基本数据类型的转换。
4. 测试案例
示例1:
int a = (int) 100.2;
100.2
是double
类型,强制转换为int
后,值变为100
,小数部分被截断。
示例2:
int b = 100;
byte c = (byte) b;
b
是int
类型,强制转换为byte
。由于b
的值在byte
的范围内(-128 到 127),因此转换不会导致数据丢失。如果b
超出byte
范围,转换后的值可能与原值不同。
示例3:
float b = 2.2f;
b = (float) (b + 3.0);
b
是float
类型,而3.0
是double
类型。由于算术运算中float
和double
参与计算时,结果会提升为double
,因此需要使用(float)
将结果强制转换为float
。
总结:
- 强制类型转换在 Java 中用于手动调整不兼容的类型,但需要注意转换过程中可能的精度损失或数据丢失。
- 基本类型的强制转换主要用于从较大范围的类型转换到较小范围的类型,例如从
double
到int
。 - 引用类型的强制转换适用于父类和子类之间的转换,但在使用时需要格外小心类型不匹配的异常情况。
016. Java(关键字和保留字)
1. 什么是关键字?
- 关键字是在编程语言中具有特殊含义的保留词,编译器会识别这些词语,并用来执行特定的操作。在
Java
中,关键字是保留的词汇,不能作为变量、类名或方法名使用。例如:
int
、if
、for
、class
等。
2. 什么是保留字?
- 保留字是编程语言中保留但可能未被赋予特定功能的词汇。它们不能作为标识符使用(如变量、类名等),但是在某些语言中可能并未实现具体功能。保留字通常是为了将来扩展语言功能而预留的。例如,在一些语言中,
goto
是保留字,即使没有被实现特定功能。
3. 关键字和保留字分别有哪些?
这个问题没有直接解释答案,而是提供了两个参考链接,分别是 Oracle 官方文档和百度百科的相关页面。通过这些链接,你可以查看 Java 中关键字和保留字的完整列表。
提供的参考链接:
017. Java(标识符)
1. 什么是标识符?
- 标识符是用于标识变量、方法、类等程序元素的名称。标识符区分大小写,也就是说在
Java 中,
myVariable
和MyVariable
是两个不同的标识符。
2. 命名规则
- 标识符可以由字母、数字、下划线
*
_
*** 和美元符号 ****$
** 组成。 - 数字不能作为标识符的开头,即标识符不能以数字开头。
- 标识符不能使用 Java 关键字和保留字。例如,不能使用
class
或goto
作为标识符。
3. 命名规范
- 驼峰命名法和下划线命名法是常见的命名规范:
- 包名:通常全部小写。
- 类名和接口名:大驼峰(即每个单词的首字母大写)。XiaojianbangName
- 变量名和方法名:小驼峰(即第一个单词的首字母小写,后续单词首字母大写)。xiaojiangbangName
- 常量名:全大写,多个单词用下划线连接。Xiaojianbang_Name
4. 判断标识符是否正确
xiaojianbang
:正确,符合规则。1xiaojianbang
:错误,标识符不能以数字开头。p-n
:错误,-
(减号)不能作为标识符的一部分。a b
:错误,空格不能用于标识符。$s_a
:正确,$
和_
可以用于标识符。class
:错误,class
是 Java 的关键字,不能作为标识符使用。goto
:错误,goto
是保留字,不能作为标识符使用。
018. Java(常量)
1. 整数常量
- 二进制、八进制和十六进制表示:
- 二进制:使用
0b
开头表示,例如0b1010
。 - 八进制:使用
0
开头表示,例如010
。 - 十六进制:使用
0x
开头表示,例如0x1F
。
- 二进制:使用
2. 小数常量
- 小数常量是用来表示浮点数的,例如
3.14
或2.71
。
3. 布尔常量
- 布尔类型的常量只有两个值:
true
和false
。
4. 字符常量
- 字符常量表示一个单个字符,用单引号
(
'
) 包裹。例如,'A'
是一个字符常量。
5. 字符串常量
- 字符串常量表示一个或多个字符组成的字符串,用双引号
(
"
) 包裹。例如,"Hello, World!"
是一个字符串常量。
6. null 常量
null
是 Java 中的空引用常量,表示没有指向任何对象。
总结:
- 整数常量可以用多种进制表示,常见的是二进制、八进制和十六进制。
- 字符和字符串常量的区别在于字符常量用单引号包裹,而字符串常量用双引号包裹。
- 布尔常量是逻辑常量,只有
true
和false
。 null
常量表示没有对象或空引用。
019. Java(算术运算符1)
1. 运算符的定义
- 运算符是一种特殊的符号,用于表示数据的运算、赋值和比较等操作。例如:
+
、-
、*
、/
等。
2. +
作为单元运算符存在时
+
** 作为单元运算符**表示正数(在不出现时默认是正数,因此通常省略)。如+5
就等价于5
。
3. +
作为二元运算符存在时
- 当左右两边没有字符串时,
+
表示加法运算。 - 当左右两边有字符串时,
+
表示字符串拼接。如果两边不是字符串,则会将它们转换成字符串进行拼接。
示例:
System.out.println(100 + 98); // 输出 198,进行加法运算
System.out.println('a' + 1); // 输出 98,因为 'a' 的 ASCII 值为 97,97 + 1 = 98
System.out.println("a" + 1 + 3); // 输出 a13,首先 "a" 是字符串,进行拼接,1 和 3 变成字符串
System.out.println(1 + 3 + "a"); // 输出 4a,1 + 3 = 4,接着与 "a" 进行拼接
4. 基本运算符 + - * /
加法、减法、乘法、除法
运算:
- 整数除法:如果两个操作数都是整数,除法的结果也是整数,余数会被舍去。
- 浮点数除法:如果任一操作数是浮点数,结果将保留小数。
示例:
System.out.println(12 / 5); // 输出 2,整数除法,余数被舍去
System.out.println(12.0 / 5); // 输出 2.4,浮点数除法
double a = 12 / 5;
System.out.println(a); // 输出 2.0,因为 12 / 5 是整数除法,结果为 2,再赋值给 double 变量,变成 2.0
int x = 4270;
x = x / 1000 * 1000;
System.out.println(x); // 输出 4000,先进行 4270 / 1000 = 4,4 * 1000 = 4000
总结:
+
运算符可以用于数值加法或字符串拼接。- 整数除法会舍弃小数部分,而浮点数除法保留小数。
- 在数学表达式中要注意数据类型的提升和转换,避免产生不期望的结果。
020. Java(算术运算符2)
4. 取模运算符 %
%
运算符的作用是计算两个数相除后的余数。表达式a % b
的结果等价于a - a / b * b
。
示例:
System.out.println(10 % 4); // 输出 2,10 除以 4 余数为 2
System.out.println(-10 % 4); // 输出 -2,-10 除以 4,余数为 -2
System.out.println(10 % -4); // 输出 2,10 除以 -4,余数为 2
System.out.println(-10 % -4); // 输出 -2,-10 除以 -4,余数为 -2
- 当涉及负数时,余数的符号与被除数(
a
)的符号相同。
5. 递增递减运算符 ++
和
--
独立使用时,
前++
和后++
的效果是相同的:都等价于i = i + 1
。在表达式中使用
时:
- 前++:先自增后取值。
- 后++:先取值后自增。
示例:
int i = 1;
i = i++;
System.out.println(i); // 输出 1,i++ 先取值后自增,i 的值被赋回 1
int i = 1;
i = ++i;
System.out.println(i); // 输出 2,++i 先自增后取值
int x = 4;
int y = (--x) + (x--) + (x * 10); //第一项是3,第二项在运算时是3,第三项是2*10
// x 先减 1 变为 3,然后用 3 参与计算后再次减 1,最终 x = 2
System.out.println(y); // 输出 26
6. 练习题
- 43 天等于多少星期零多少天?
- 用
43 / 7
计算完整的星期数,用43 % 7
计算剩余的天数。
- 用
- 4725 秒等于多少小时多少秒?
- 用
4725 / 3600
计算完整的小时数,用4725 % 3600
计算剩余的秒数。
- 用
- 摄氏温度 c 转换为华氏温度 f 的公式:
总结:
%
运算符用于取余数,注意余数的符号与被除数相同。++
和--
在独立使用时效果相同,在表达式中则需注意它们的自增/自减时机。
021. Java(算术运算符练习)
6. 练习题
43 天等于多少星期零多少天?
- 用
43 / 7
计算完整的星期数,用43 % 7
计算剩余的天数。
- 用
4725 秒等于多少小时多少秒?
- 用
4725 / 3600
计算完整的小时数,用4725 % 3600
计算剩余的秒数。
- 用
摄氏温度 c 转换为华氏温度 f
的公式:
- f=95×c+32f = c + 32f=59×c+32
public class Hello{
public static void main(String[] args) {
// 43天等于多少星期多少天
int days = 43;
int weeks = days / 7;
int day = days % 7;
System.out.println(weeks + " 星期 " + day + " 天");
// 4725秒等于多少小时分钟秒
int times = 4725;
int hours = times / 3600;
System.out.println(hours + " 小时");
int temp = times % 3600;
System.out.println(temp + " 秒余");
int minutes = temp / 60;
System.out.println(minutes + " 分钟");
int second = temp % 60;
System.out.println(second + " 秒");
// 摄氏温度转换为华氏温度,公式:f = 9/5 * c + 32
float c = 37;
float f = 9.0f / 5 * c + 32;
System.out.println(f + " 华氏温度");
}
}
022. Java(赋值运算符)
1. 赋值运算符的定义
- 赋值运算符是将右边的值运算后赋值给左边的变量。
2. 赋值运算符的左边和右边
- 赋值运算符的左边只能是变量,而右边可以是变量、表达式、常量等。
3. 赋值运算符的分类
- 基本赋值运算符:
=
- 示例:
int a = 100;
表示将100
赋值给变量a
。
- 示例:
- 复合赋值运算符:
+=
,-=
,*=
,/=
,%=
- 示例:
a += 2;
等价于a = a + 2;
,即将a
的值加上2
,再将结果赋值给a
。
- 示例:
4. 有趣的案例
byte b = 3;
b = b + 4; // 错误:因为 b + 4 的结果是 int 类型,不能直接赋值给 byte
- 在 Java 中,
byte
和short
参与算术运算时会自动提升为int
,因此b + 4
的结果是int
类型,不能直接赋值给byte
类型,必须显式进行类型转换。
byte b = 3;
b += 4; // 正确:复合赋值运算符内部会自动进行类型转换
- 使用复合赋值运算符
+=
时,Java 会自动进行类型转换,将结果赋值给b
,因此这是合法的。
byte b = 3;
b++; // 正确:自增运算符会自动处理类型转换
b++
是合法的,自增运算符会自动处理byte
类型的值,不需要显式进行类型转换。
byte b = 3;
b = b + 1; // 错误:同样原因,b + 1 的结果是 int,不能赋值给 byte
- 由于
b + 1
的结果是int
,必须显式转换为byte
,否则会导致编译错误。
总结:
- 复合赋值运算符(如
+=
)可以自动处理类型转换。 - 自增/自减运算符也会自动处理类型转换。
- 但是,对于普通的算术运算(如
b + 1
),结果是int
类型,不能直接赋值给byte
,需要显式类型转换。
023. Java(关系运算符)
1. 关系运算符的结果
- 关系运算符(也称为比较运算符)的结果是
boolean
类型,要么是true
,要么是false
。这些运算符用于比较两个值,并根据比较结果返回布尔值。
2. 关系表达式
- 关系运算符组成的表达式,称为关系表达式。这些表达式通常用于
if
结构中的条件判断或循环结构中的条件判断。
3. 常见的关系运算符
==
:相等,表示两个值是否相等(注意不能写成单等号=
,单等号是赋值运算符)。!=
:不等于,表示两个值是否不相等。<
:小于。>
:大于。<=
:小于等于。>=
:大于等于。instanceof
:用于判断对象是否是某个类的实例。A instanceof B
表示A
是否是B
这个类的对象。
示例:
int x = 5;
int y = 10;
System.out.println(x == y); // 输出 false
System.out.println(x != y); // 输出 true
System.out.println(x < y); // 输出 true
System.out.println(x > y); // 输出 false
System.out.println(x <= y); // 输出 true
System.out.println(x >= y); // 输出 false
// 使用 instanceof 判断对象类型
String str = "Hello";
System.out.println(str instanceof String); // 输出 true
总结:
- 关系运算符用于比较两个值,返回
true
或false
。 - 常见的运算符包括相等(
==
)、不等于(!=
)、小于、大于等运算符。 instanceof
运算符用于判断某个对象是否是特定类的实例。
024. Java(逻辑运算符)
1. 关系表达式的结果
- 关系运算符可以连接多个关系表达式,最终的结果是一个
boolean
值(即true
或false
)。
2. 逻辑运算符
- 短路与
&&
和 逻辑与&
:用于判断两个表达式是否同时为true
。 - 短路或
||
和 逻辑或|
:用于判断两个表达式中是否有一个为true
。 - 取反
!
:将布尔值取反,true
变为false
,false
变为true
。 - 逻辑异或
^
:用于判断两个表达式的真假性是否不同。
3. 逻辑运算的规则
a & b
:当a
和b
同时为true
时,结果为true
,否则为false
。a && b
:当a
和b
同时为true
时,结果为true
,否则为false
。a | b
:当a
和b
有一个为true
时,结果为true
,否则为false
。a || b
:当a
和b
有一个为true
时,结果为true
,否则为false
。a ^ b
:当a
和b
不同时,结果为true
,否则为false
。!a
:如果a
为true
,结果为false
;如果a
为false
,结果为true
。
4. 逻辑与短路运算的区别
&
与&&
的区别:&
:无论左边是否为true
或false
,都会对右边进行计算。&&
:如果左边为false
,右边就不会再运算(短路特性)。
|
与||
的区别:|
:无论左边是否为true
或false
,都会对右边进行计算。||
:如果左边为true
,右边就不会再运算(短路特性)。
示例代码:
int a = 5;
int b = 10;
System.out.println(a > 3 && b < 15); // 输出 true,短路与,两个条件都为 true
System.out.println(a > 3 || b > 20); // 输出 true,短路或,左边为 true,所以右边不再计算
System.out.println(!(a > 3)); // 输出 false,取反,a > 3 是 true,取反后为 false
System.out.println(a > 3 ^ b < 5); // 输出 true,异或运算,a > 3 为 true,b < 5 为 false,结果为 true
总结:
- 短路运算符(
&&
和||
)具有短路特性,即当第一个条件已经能够确定最终结果时,第二个条件不再计算。 - 逻辑与和逻辑或(
&
和|
)会无论如何都计算左右两边的表达式。 - 逻辑运算符用于复杂条件的组合和判断,返回的结果是布尔类型
boolean
。
025. Java(逻辑运算符练习)
第一个代码块:
int x = 100, y = 200;
if (x++ == 100 && ++y == 200) {
x = 10;
}
System.out.println("x=" + x + ", y=" + y);
解析:
x++ == 100
:x++
是后置自增运算,先进行比较,然后x
自增。- 因此,
x == 100
为true
,之后x
变为101
。
++y == 200
:++y
是前置自增运算,先自增后比较。- 因此,
y
变为201
,比较结果为false
。
&&
短路:- 因为
&&
是短路与运算,前面的条件为true
,但是后面的条件为false
,所以整个表达式为false
,x = 10
不会执行。
- 因为
输出结果:
x = 101, y = 201
第二个代码块:
int x = 100, y = 200;
if (x++ == 100 || ++y == 200) {
x = 10;
}
System.out.println("x=" + x + ", y=" + y);
解析:
x++ == 100
:- 与上面相同,
x == 100
为true
,x
变为101
。
- 与上面相同,
||
短路:- 因为
||
是短路或运算,前面的条件已经为true
,所以右边的条件++y == 200
不会再执行,y
保持200
不变。 x = 10
会执行。
- 因为
输出结果:
x = 10, y = 200
第三个代码块:
boolean x = true;
boolean y = false;
short z = 100;
if ((z++ == 100) && (y = true)) z++;
if ((x = false) || (++z == 100)) z++;
System.out.println("z=" + z);
解析:
z++ == 100
:z++
先比较再自增,因此z == 100
为true
,之后z
变为101
。
y = true
:- 这个表达式将
y
赋值为true
,并返回true
,因此整个&&
条件为true
,所以z++
执行,z
变为102
。
- 这个表达式将
x = false
:- 这个表达式将
x
赋值为false
,因此整个条件的左边为false
。
- 这个表达式将
||
短路:- 因为左边为
false
,右边++z == 100
继续执行。z
变为103
,但是103 == 100
为false
,所以z++
不会执行。
- 因为左边为
输出结果:
z = 103
总结:
- 短路与 (
&&
) 和 短路或 (||
) 的使用时,如果前面的条件已经决定结果,后面的条件就不会执行。 - 后置自增 (
x++
) 和 前置自增 (++x
) 的区别在于,前置自增先自增再比较,后置自增先比较再自增。
026. Java(三元运算符)
三目运算符(也叫条件运算符)的用法,具体内容如下:
1. 基本语法
- 三目运算符的语法:
变量 = (条件表达式) ? 表达式1 : 表达式2;
- 如果**条件表达式**为 `true`,则执行**表达式1**;否则,执行**表达式2**。
- 三目运算符是 `if...else` 语句的简写形式。
2. 案例
int x = 3;
x = x > 3 ? 1 : 2;
System.out.println(x);
- 解释:
- 条件表达式
x > 3
,即3 > 3
,结果为false
。 - 因为条件为
false
,所以执行表达式2,将2
赋值给x
。 - 输出结果为
2
。
- 条件表达式
3. 练习
- 练习题:求三个数中的最大数。
- 可以使用嵌套的三目运算符来实现三个数中的最大数。
- 例如:
int a = 5, b = 8, c = 3;
int max = (a > b) ? (a > c ? a : c) : (b > c ? b : c);
System.out.println("最大值是: " + max);
总结:
- 三目运算符是简洁的
if...else
语句,适合在需要简化代码的情况下使用,但不宜过度嵌套以免代码难以理解。 - 练习题展示了如何使用三目运算符求三个数的最大值。
027. Java(运算符优先级)
Java 中运算符的优先级,具体解释如下:
1. 运算符优先级从上到下
- 表格中列出的运算符按从上到下的顺序排列,越靠上,优先级越高。优先级决定了在没有括号的情况下,表达式中运算符的执行顺序。
2. 运算方向
- 单元运算符(如
++
、--
等)和赋值运算符(如=
、+=
等)的计算顺序是从右向左。 - 其他运算符的计算顺序是从左向右。
3. 括号的使用
- 如果不清楚某个运算符的优先级,或者想确保运算顺序,可以使用小括号来改变优先级。括号内的表达式会先计算。
优先级表格解析
- 最高优先级:括号
()
、单目运算符(如++
、--
、!
、~
)等。 - 乘法、除法、取模运算:
*
、/
、%
优先级高于加法和减法。 - 加法和减法:
+
、-
优先级低于乘法。 - 移位运算:
<<
、>>
、>>>
。 - 关系运算符:小于、大于、
<=
、>=
以及instanceof
。 - 相等运算符:
==
、!=
。 - 逻辑运算符:按顺序为
&
、^
、|
、&&
、||
。 - 条件运算符:三目运算符
? :
。 - 赋值运算符:
=
及其复合赋值运算符(如+=
、-=
等)优先级最低。
总结:
- 记住运算符的优先级有助于理解复杂表达式的执行顺序。但如果不确定优先级,使用小括号可以确保代码按预期顺序执行。
028. Java(进制与位运算符)
1. 整数的四种表示方法
- 十进制:由
0-9
表示,不需要特殊前缀。 - 二进制:由
0
和1
组成,表示时以0b
或0B
开头。 - 八进制:由
0-7
组成,表示时以0
开头。 - 十六进制:由
0-9
和a-f
(或A-F
)组成,表示时以0x
或0X
开头。
2. 示例代码
int a1 = 100; // 十进制
int a2 = 0b01100100; // 二进制表示的 100
int a3 = 0144; // 八进制表示的 100
int a4 = 0x64; // 十六进制表示的 100
- 这些不同进制的表示方法,最终的值都是
100
,只是以不同的进制格式表示。
3. 位运算符
&
:按位与运算。|
:按位或运算。^
:按位异或运算。~
:按位取反。<<
:左移运算符,向左移动指定的位数。>>
:右移运算符,带符号右移,保留符号位。>>>
:无符号右移,不保留符号位,左边补0
。
示例:
int x = 6; // 0110 二进制
int y = 3; // 0011 二进制
// 按位与
int result1 = x & y; // 0010,结果为 2
// 按位或
int result2 = x | y; // 0111,结果为 7
// 按位异或
int result3 = x ^ y; // 0101,结果为 5
// 按位取反
int result4 = ~x; // 取反结果为 -7
总结:
- 不同进制表示方式提供了对数值的不同表示形式,但表示的数值可以是相同的。
- 位运算符对二进制位进行操作,常用于低级别的位控制和优化。
029. Java(进制转换)
- 十进制转十六进制
- 将该数不断除以16,直到商为0,每一步得到的余数倒过来,就是对应的十六进制数。
- 二进制转十进制
- 1个字节等于8位(bit)。另外指出了Java中的数字默认是有符号的。
- 二进制转十六进制
- 从低位开始,将二进制数每四位一组,转换成对应的十六进制数即可。
- Hex编码
- 这里提到的是十六进制编码的概念。
030. Java(Hex编码)
这段代码的主要功能是将字符串转换为MD5哈希值,并且使用HexBin.encode
将生成的MD5摘要进行编码为十六进制格式。
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.apache.commons.codec.binary.Hex;
public class Hello {
public static void main(String[] args) {
byte[] strs = "a12345678".getBytes();
for (int i = 0; i < strs.length; i++) {
System.out.println(strs[i]);
}
System.out.println("new String strs: " + new String(strs));
System.out.println("===============");
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update("a12345678".getBytes());
byte[] digest = md5.digest();
System.out.println("new String digest: " + new String(digest));
for (int i = 0; i < digest.length; i++) {
System.out.println(digest[i]);
}
// 使用Hex编码MD5摘要
System.out.println(Hex.encodeHexString(digest));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
}
代码补全和解释:
MessageDigest
使用:代码首先创建了MessageDigest
实例,并指定使用MD5
算法。然后通过update()
方法对字符串"a12345678"
的字节数组进行更新,接着调用digest()
方法获取MD5摘要。- Hex编码:通过
Hex.encodeHexString()
将生成的MD5摘要转换为十六进制字符串。图片中提到的HexBin.encode()
可能来自于某个库(例如javax.xml.bind.DatatypeConverter
),不过更常用的是使用Apache Commons Codec
库中的Hex
类。 - 捕获异常:代码捕获了
NoSuchAlgorithmException
,防止MD5算法在某些环境中不存在时抛出错误。
这段代码的主要流程就是从一个字符串生成其MD5摘要,并将其以十六进制形式输出。
-23 1110 1001 0xE9
14 0000 1110 0x0E
031. Java(if语句)
- 程序的三种结构:
- 顺序结构:程序按顺序执行。
- 分支结构:根据条件执行不同的代码块。
- 循环结构:重复执行某段代码,直到满足某个条件。
- 分支的分类:
- 单分支
if
:例如,当公交卡的money < 10
时,提示余额不足,建议充值,否则不提示。 - 双分支
if...else
:例如,当money > 2
时,提示刷卡成功,否则提示余额不足。 - 多分支
if...else if...else
:用于处理多个条件的分支结构。
- 单分支
- 连续
if
和if...else if...else
的区别:解释了如何区分多个条件的情况。 if
的嵌套:- 例如,在
money > 2
时提示刷卡成功,并且seat == 1
时表示有座位。这是嵌套if
的一个例子。
- 例如,在
- if、switch、while、do...while、for:
- 这部分提到了几种常见的控制流语句,包括
if
、switch
、while
、do...while
和for
循环。并且说明如果代码块中只有一条语句时,可以省略大括号。
- 这部分提到了几种常见的控制流语句,包括
- 根据定义的数值不同,打印对应的星期:
- 这一部分介绍了一个场景,要求根据给定的数值来打印星期几,但具体代码没有展示。大概率是使用
switch
或者if
语句来判断数值范围(如1-7)以输出对应的星期。
import java.util.Scanner; public class WeekdayPrinter { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("请输入一个 1 到 7 的数字:"); int day = scanner.nextInt(); if (day == 1) { System.out.println("星期一"); } else if (day == 2) { System.out.println("星期二"); } else if (day == 3) { System.out.println("星期三"); } else if (day == 4) { System.out.println("星期四"); } else if (day == 5) { System.out.println("星期五"); } else if (day == 6) { System.out.println("星期六"); } else if (day == 7) { System.out.println("星期日"); } else { System.out.println("输入的数字无效,请输入 1 到 7 之间的数字。"); } scanner.close(); } }
- 这一部分介绍了一个场景,要求根据给定的数值来打印星期几,但具体代码没有展示。大概率是使用
- 根据定义的月份不同,打印该月份所属的季节:
- 给出了一段代码示例,根据月份打印所属季节:
int x = 3; if (x > 12 || x < 1) System.out.println("月份不存在"); else if (x >= 3 && x <= 5) System.out.println("春季"); else if (x >= 6 && x <= 8) System.out.println("夏季"); else if (x >= 9 && x <= 11) System.out.println("秋季"); else System.out.println("冬季");
- 代码中通过
if...else if...else
结构判断月份x
的值,输出对应的季节。如果月份大于12或小于1,打印“月份不存在”;如果是3到5月,打印“春季”;6到8月是“夏季”;9到11月是“秋季”;其余情况(即1、2和12月)打印“冬季”。
032. Java(switch语句)
switch
的基本语法:- 关键字:
switch
、case
、break
、default
。 case
和default
是可选的。switch
语句根据变量的值,执行相应的case
分支。
- 关键字:
根据定义的数值不同,打印对应的星期:
- 没有给出具体的代码示例,但这一部分显然是在说明如何使用
switch
语句来根据数值(例如1到7)输出对应的星期。
import java.util.Scanner; public class WeekdayPrinter { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("请输入一个 1 到 7 的数字:"); int day = scanner.nextInt(); switch (day) { 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; default: System.out.println("输入的数字无效,请输入 1 到 7 之间的数字。"); break; } scanner.close(); } }
- 没有给出具体的代码示例,但这一部分显然是在说明如何使用
加不加
break
的区别:- 如果在
case
语句中没有加break
,程序将继续执行后续的case
语句,直到遇到break
或switch
块结束。反之,如果加了break
,程序会跳出switch
块,不会继续执行后续的case
语句。
- 如果在
根据定义的月份不同,打印该月份所属的季节:
- 代码示例:
int x = 4; switch (x) { case 3: case 4: case 5: System.out.println("春季"); break; }
- 这个代码片段展示了如何根据月份
x
的值输出所属的季节(春季)。case 3: case 4: case 5:
表明当x
为3、4或5时,都会打印“春季”,并且使用了break
来防止后续代码的执行。
一般情况下,case块的顺序可以调换:
- 说明在
switch
语句中,各个case
分支的顺序是可以任意调整的,执行顺序由表达式匹配的结果决定,而不依赖case
的书写顺序。
- 说明在
switch
中表达式的数据类型,要和case
后面常量类型一致,或者是可以转换的:switch
语句中,表达式的结果类型必须与case
后面提供的常量类型相同,或者是可以进行类型转换的(如隐式类型转换)。
switch
中表达式返回的类型,只能是byte
、short
、int
、char
、String
、enum
:switch
语句中的表达式只支持这几种类型:byte
、short
、int
、char
、String
和枚举(enum
)。因此,浮点数类型(如float
和double
)不被支持。
case
后面的值,只能是常量或者常量表达式,不能是变量:case
语句后面的值必须是一个编译时常量,不能使用变量作为case
的判断值。这意味着你不能在case
中使用一个动态计算出的值或从输入获取的变量值。
033. Java(while循环)
1. while
基本语法
循环变量初始化
while (循环条件) {
执行语句;
循环变量迭代
}
- 循环变量初始化:你需要先定义一个变量作为循环控制变量。
- 循环条件:
while
循环会在条件为True
时不断执行循环体中的代码。 - 循环变量迭代:每次循环后,更新循环变量的值,使其逐渐接近循环终止条件。
- 执行顺序:先判断条件,再决定是否执行循环体。
2. 执行流程
- 初始化循环变量。
- 判断循环条件是否成立,成立则继续。
- 执行循环体的代码。
- 更新循环变量。
- 再次判断循环条件,重复该过程直到条件不成立。
3. 计算 1 到 100 的和
- 这是一个常见的用
while
循环来实现累加和的例子。循环变量从 1 开始,每次加上当前的值,直到累加到 100。
public class SumExample {
public static void main(String[] args) {
int sum = 0;
int i = 1; // 初始化循环变量
while (i <= 100) { // 循环条件
sum += i; // 执行语句
i++; // 循环变量迭代
}
System.out.println("1 到 100 的和是: " + sum);
}
}
4. 计算 1 到 100 的偶数和
- 这个问题也是通过
while
循环实现,不过只累加 1 到 100 之间的偶数。这可以通过判断数字是否为偶数来控制累加逻辑。
public class EvenSumExample {
public static void main(String[] args) {
int sum = 0;
int i = 1; // 初始化循环变量
while (i <= 100) { // 循环条件
if (i % 2 == 0) { // 检查是否为偶数
sum += i; // 如果是偶数则加到sum
}
i++; // 循环变量迭代
}
System.out.println("1 到 100 的偶数和是: " + sum);
}
}
问题 5:计算 1 到 100 之间 7 的倍数的个数
public class MultiplesOf7 {
public static void main(String[] args) {
int count = 0; // 统计7的倍数的个数
int i = 1; // 初始化循环变量
while (i <= 100) { // 循环条件
if (i % 7 == 0) { // 检查是否为7的倍数
count++; // 是7的倍数,计数加1
}
i++; // 循环变量递增
}
System.out.println("1 到 100 之间 7 的倍数的个数是: " + count);
}
}
解释:
- 通过
while
循环遍历 1 到 100 之间的数,使用i % 7 == 0
来判断是否是 7 的倍数。 - 每当遇到 7 的倍数时,计数器
count
加 1。
问题 6:3000 米长的绳子,每天减一半,多少天后绳子小于 5 米?
public class RopeLength {
public static void main(String[] args) {
double length = 3000; // 初始绳子长度为3000米
int days = 0; // 初始化天数为0
while (length >= 5) { // 当长度大于等于5米时
length /= 2; // 每天减半
days++; // 天数加1
}
System.out.println("绳子长度小于5米所需的天数是: " + days);
}
}
解释:
- 每天将绳子的长度减半,直到绳子的长度小于 5 米。
- 使用
while
循环,直到长度小于 5 米时停止循环,并输出所需的天数。
问题 7:do...while
基本语法
为了演示 do...while
语法,可以使用以下代码来实现简单的循环。
public class DoWhileExample {
public static void main(String[] args) {
int i = 1; // 初始化循环变量
do {
System.out.println("当前数字是: " + i);
i++; // 循环变量递增
} while (i <= 5); // 循环条件,直到i大于5时停止
}
}
解释:
do...while
循环是先执行一次循环体,再判断循环条件。因此,即使初始条件不满足,循环体至少会执行一次。- 在这个例子中,从
i = 1
开始,输出数字,直到i
大于 5 停止循环。
034. Java(for循环)
1. for
循环的基本语法
for (初始化表达式; 循环条件表达式; 循环后的操作表达式) {
执行语句;
}
- 初始化表达式:在循环开始时执行一次,用于定义循环控制变量。
- 循环条件表达式:在每次循环之前进行判断,如果条件为
true
,则执行循环体,否则退出循环。 - 循环后的操作表达式:每次循环结束后,执行的操作(例如增加或减少控制变量)。
2. for
循环的执行流程
- 初始化表达式:执行一次,用于初始化循环控制变量。
- 循环条件表达式:判断循环是否继续。
- 执行语句:如果条件为
true
,执行循环体中的代码。 - 循环后的操作表达式:每次循环体执行完后,执行操作表达式(通常是更新控制变量)。
- 循环条件表达式:再次检查循环条件,直到条件为
false
停止循环。
3. for
循环的注意事项
- 可以放任意表达式:初始化、条件、和操作表达式均可以放任意有效的表达式。
- 循环条件表达式必须返回
boolean
类型:即循环条件表达式必须返回true
或false
,例如i < 10
。 - 多个表达式用逗号隔开:如果有多个初始化或操作表达式,它们必须用逗号分隔。
- 作用域:
for
语句中的变量具有局部作用域。如果变量在for
循环中声明,当循环结束后,这个变量会在内存中释放。
035. Java(break与continue)
1. 常见的循环控制方法
break
: 用于直接跳出当前的循环。循环中一旦遇到break
,程序将立即退出该循环,并执行循环之后的代码。示例:
for (int i = 1; i <= 10; i++) {
if (i == 5) {
break; // 当 i == 5 时,跳出循环
}
System.out.println(i);
}
输出:
1
2
3
4
continue
: 用于跳过当前循环的剩余部分,直接进入下一次循环的判断条件。当遇到continue
时,程序会跳过当前迭代的剩余代码,进入下一次循环。示例:
for (int i = 1; i <= 5; i++) {
if (i == 3) {
continue; // 当 i == 3 时,跳过此次循环
}
System.out.println(i);
}
输出:
1
2
4
5
return
: 用于跳出当前所在的方法。如果return
写在main
方法中,它将直接退出整个程序。示例:
public static void main(String[] args) {
for (int i = 1; i <= 10; i++) {
if (i == 5) {
return; // 当 i == 5 时,退出 main 方法,程序终止
}
System.out.println(i);
}
System.out.println("循环结束"); // 这行代码永远不会执行
}
输出:
1
2
3
4
2. 不推荐的使用方法
带标签的
break
和continue
:Java 支持给循环加上标签(label
),然后通过break label;
或continue label;
来跳出或继续指定的循环。这种方法尽管可行,但由于可读性差,通常不推荐使用。带标签的
break
示例:
outerLoop: // 标签
for (int i = 1; i <= 5; i++) {
for (int j = 1; j <= 5; j++) {
if (i * j == 9) {
break outerLoop; // 跳出外层循环
}
System.out.println("i=" + i + " j=" + j);
}
}
带标签的 continue
示例:
outerLoop:
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
if (j == 2) {
continue outerLoop; // 跳过外层循环的下一次迭代
}
System.out.println("i=" + i + " j=" + j);
}
}
解释:
- 使用标签可以直接控制嵌套的外循环,而不仅仅是控制当前的内层循环。但这种方式容易导致代码难以阅读,因此通常建议通过调整代码结构来避免使用标签。
036. Java(数组)
1. 为什么需要数组
- 在某些情况下,比如定义26个
char
类型的变量来存储字母,这会导致冗长的代码和不便管理。 - 数组允许你一次性定义多个相同类型的变量,并且可以通过下标来访问这些变量,简化了操作和管理。
示例:
char[] letters = new char[26]; // 定义一个长度为26的char数组
2. 数组的概念
- 数组是多个相同类型数据的集合,用来统一管理这些数据。
- 数组中的元素可以是任意基本数据类型或引用类型,但同一个数组中的元素必须是同类型的,不能混用。
- 数组下标从
0
开始,因此可以通过下标快速访问每个元素。
示例:
int[] numbers = {1, 2, 3, 4, 5}; // 一个存储5个整数的数组
System.out.println(numbers[0]); // 输出数组的第一个元素,即1
3. 数组的语法
动态初始化
- 动态初始化指的是只指定数组的长度,而不赋初值。
- 数组在创建时每个元素会自动初始化为该数据类型的默认值(例如,整型为
0
,布尔型为false
,引用类型为null
)。
语法:
int[] arr = new int[5]; // 创建一个长度为5的int数组
静态初始化
- 静态初始化指的是在定义数组时,直接指定数组中的元素值。
语法:
int[] arr = new int[] {1, 2, 3, 4, 5}; // 创建并初始化数组
也可以省略 new int[]
,写作:
int[] arr = {1, 2, 3, 4, 5};
手动赋值
- 你可以在定义数组后,通过下标为每个元素赋值。
示例:
int[] arr = new int[5]; // 创建一个长度为5的int数组
arr[0] = 1; // 手动赋值
arr[1] = 2;
arr[2] = 3;
arr[3] = 4;
arr[4] = 5;
小结
- 数组可以方便地管理一组相同类型的数据,避免了多个单独变量的定义。
- Java 支持数组的动态初始化和静态初始化两种方式,能够灵活满足不同需求。
4. 数组的默认值
当数组被创建后,如果没有赋值,数组元素会被初始化为各自数据类型的默认值:
long
/int
/short
/byte
: 默认值是0
float
/double
: 默认值是0.0
char
: 默认值是'\u0000'
(即空字符)boolean
: 默认值是false
String
(或任何引用类型): 默认值是null
示例:
int[] arr = new int[5]; // 创建一个长度为5的int数组
System.out.println(arr[0]); // 输出0,这是默认值
5. 数组的使用步骤
使用数组的一般步骤如下:
- 声明数组:定义数组的类型和名称。
- 开辟空间:通过
new
关键字为数组分配内存空间。 - 给数组各个元素赋值:可以手动或通过循环给数组的每个元素赋值。
- 使用数组:通过下标访问和使用数组元素。
示例:
int[] arr = new int[5]; // 声明并开辟空间
arr[0] = 10; // 给第一个元素赋值
System.out.println(arr[0]); // 使用数组,输出10
6. 数组成员(元素)的赋值和读取
- 通过数组下标可以对数组的元素进行赋值和读取。
- 数组下标从
0
开始。
示例:
int[] arr = new int[3];
arr[0] = 1; // 赋值
arr[1] = 2;
arr[2] = 3;
System.out.println(arr[1]); // 读取元素,输出2
7. 数组下标越界异常
数组的下标从 0
开始,如果访问的下标超出了数组的长度,程序将抛出
ArrayIndexOutOfBoundsException
。
示例:
int[] arr = new int[3];
System.out.println(arr[3]); // 这里会抛出数组下标越界异常,因为最大下标是2
空指针异常:
如果数组被声明但没有初始化(即没有为其分配内存空间),尝试访问它的元素将会抛出
NullPointerException
。
示例:
int[] arr; // 声明但没有初始化
System.out.println(arr[0]); // 这里会抛出空指针异常
小结
- 数组中的元素有默认值,具体取决于数据类型。
- 数组的下标从
0
开始,越界访问会导致异常。 - 数组必须先初始化才能使用,否则会抛出空指针异常。
037. Java(数组遍历)
8. 数组遍历
- 获取数组长度:通过
array.length
属性可以获取数组的长度。长度是数组中元素的个数。 - 获取数组中最大的值:可以通过遍历数组来找到数组中的最大值。
- 数组扩容:在 Java 中,数组是固定大小的,不能直接扩容。如果需要扩展数组,可以创建一个新的更大的数组,并将旧数组中的元素复制到新的数组中。
获取数组长度与最大值的示例:
public class ArrayOperations {
public static void main(String[] args) {
int[] arr = {1, 3, 5, 2, 8, 6};
int max = arr[0];
// 获取数组长度
System.out.println("数组长度: " + arr.length);
// 查找最大值
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
System.out.println("数组中的最大值: " + max);
}
}
038. Java(值传递与引用传递)
9. 数组是引用类型
- 在 Java 中,数组是一种引用类型。当你将数组传递给方法时,实际上是将数组的引用传递过去,因此方法内部的修改会影响到原始数组。
引用类型的示例:
public class ReferenceType {
public static void main(String[] args) {
int[] arr = {1, 2, 3};
modifyArray(arr);
System.out.println("修改后的数组: " + arr[0]); // 输出:10
}
public static void modifyArray(int[] array) {
array[0] = 10; // 修改数组的第一个元素
}
}
modifyArray
是一个自定义方法的名称,它的功能是修改传入的数组。这个方法的作用是接收一个数组作为参数,然后对数组的内容进行修改。由于
Java 中数组是
引用类型,在方法内部对数组的修改会直接影响到传入的原数组。
具体解释:
public static void modifyArray(int[] arr) {
arr[0] = 10; // 修改数组的第一个元素为10
}
public static
:这是方法的修饰符,表示这个方法是公开的,并且是静态的(可以通过类名直接调用,无需实例化对象)。void
:表示这个方法没有返回值。modifyArray
:这是方法的名称,表示它是用来修改数组的。int[] arr
:这是方法的参数,表示接收一个int
类型的数组。参数名arr
是这个数组在方法内部的引用。
核心意义:modifyArray
方法修改传入数组的第一个元素。由于数组是引用类型,传入方法的实际上是数组的引用地址,因此在方法内部对数组元素的修改会反映到原数组。
示例代码:
public class Example {
public static void main(String[] args) {
int[] array = {1, 2, 3}; // 定义一个数组
modifyArray(array); // 调用modifyArray方法,传入数组
System.out.println(array[0]); // 输出数组的第一个元素,结果为10
}
public static void modifyArray(int[] arr) {
arr[0] = 10; // 修改数组的第一个元素为10
}
}
执行过程:
- 主方法中定义了一个数组
array
,初始值为{1, 2, 3}
。 - 调用
modifyArray(array)
方法,把array
数组传递给方法modifyArray
。 - 在
modifyArray
方法中,数组的第一个元素被修改为10
。 - 因为数组是引用类型,修改影响到主方法中的数组,所以输出的第一个元素变成了
10
。
总结:
modifyArray
方法用于修改传入的数组,由于数组是引用类型,修改会直接影响到传入的原始数组。
10. 值传递与引用传递
- 栈内存:局部变量和基本类型数据存储在栈内存中。当方法执行完毕后,栈内存中的变量会自动释放。
- 堆内存:数组和对象通过
new
关键字创建,它们被存储在堆内存中。当没有任何引用指向这些对象时,垃圾回收器(GC)会在合适的时间回收它们。 - 引用类型默认初始化为
**
null
:当引用类型没有显式初始化时,其默认值是null
。 - 方法区:存储类的信息、静态变量、常量等。
示例:值传递与引用传递:
public class ValueAndReference {
public static void main(String[] args) {
int a = 10;
int[] array = {1, 2, 3};
modifyValue(a);
modifyArray(array);
System.out.println("基本类型的值传递: " + a); // 输出:10
System.out.println("引用类型的引用传递: " + array[0]); // 输出:10
}
public static void modifyValue(int num) {
num = 20; // 改变值不会影响主方法中的变量
}
public static void modifyArray(int[] arr) {
arr[0] = 10; // 改变数组的值会影响原始数组
}
}
总结:
- 在 Java 中,数组是引用类型,传递数组时传递的是引用,因此修改数组元素会影响原始数组。
- 基本数据类型的值传递不会影响原始值,修改仅在局部有效。
039. Java(二维数组)
1. 二维数组的定义
- 方法 1:直接定义二维数组,并指定每维的长度。
int[][] arr = new int[2][3]; // 创建一个2行3列的二维数组
arr[0][1] = 78; // 给第一行第二列的元素赋值为78
- 方法 2:逐步定义各行的数组长度。
int[][] arr = new int[2][]; // 创建一个二维数组,但每行长度未定
arr[0] = new int[2]; // 给第一行分配两个元素的空间
arr[1] = new int[3]; // 给第二行分配三个元素的空间
- 方法 3:直接用静态初始化给数组赋值。
int[][] arr = {{1, 2, 3}, {4, 5, 6}}; // 创建并初始化一个2行3列的数组
2. 二维数组的长度
arr.length
:表示二维数组的行数。例如,arr.length
为 2。arr[0].length
:表示二维数组中第 1 行的列数。例如,arr[0].length
为 3。
示例:
int[][] arr = {{1, 2, 3}, {4, 5, 6}};
System.out.println(arr.length); // 输出二维数组的行数,结果为2
System.out.println(arr[0].length); // 输出第一行的列数,结果为3
3. 二维数组的遍历
可以使用两层循环来遍历二维数组:
int[][] arr = {{1, 2, 3}, {4, 5, 6}};
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(); // 每行结束后换行
}
输出:
1 2 3
4 5 6
4. 二维数组的其他定义方式
int x[];
和int[] x;
都是定义一维数组的合法方式。int[][] y;
和int y[][];
都是定义二维数组的合法方式。
注意:如果在同一行定义多个变量,方括号([]
)的位置会影响解释。例如:
int x[], y[]; // x 和 y 都是二维数组
int[] x, y; // x 和 y 都是一维数组
int x[], y; // x 是一维数组,y 是普通整数变量
小结
- 二维数组可以使用多种方式定义,包括静态和动态初始化。
- 通过
arr.length
和arr[0].length
可以分别获取二维数组的行数和列数。 - 遍历二维数组通常使用嵌套循环。
040. Java(数组练习)
1. 正确的数组定义
图片中提供了几种 String
类型数组的定义方式。让我们来分析哪些是正确的。
String strs[] = {'a', 'b', 'c'}; // 错误:应该使用双引号定义字符串,而不是单引号
String[] strs = {"a", "b", "c"}; // 正确:使用双引号定义字符串
String[] strs = new String[]{"a", "b", "c"}; // 正确:使用 new 关键字进行初始化
String strs[] = new String[]{"a", "b", "c"}; // 正确:使用数组定义的另一种形式
String[] strs = new String[3]{"a", "b", "c"}; // 错误:不能在 new String[3] 中同时指定大小和初始化内容
结论:第二、第三、第四行的定义方式是正确的,第一行和最后一行是错误的。
2. 输出的结果
代码如下:
String foo = "blue";
boolean[] bar = new boolean[2]; // 创建一个长度为2的boolean数组,默认值为false
if(bar[0]) { // bar[0] 的值为 false,所以不会进入这个if语句
foo = "green";
}
System.out.println(foo); // 输出foo的值
分析:
boolean
类型数组默认值为false
,因此bar[0]
为false
,不会进入if
语句。- 变量
foo
保持初始值"blue"
不变。
输出结果:
blue
3. 输出的结果
代码如下:
char[] arr1 = {'a', 'b', 'c', 'd'};
char[] arr2 = arr1; // arr2 和 arr1 指向同一个数组
arr2[2] = 'x'; // 修改 arr2 的第3个元素,实际上 arr1 也会被修改
System.out.println(arr1[2]); // 输出 arr1 的第3个元素
分析:
arr2 = arr1
表示arr2
和arr1
共享同一个数组内存空间。- 修改
arr2[2]
也会影响arr1
,因此arr1[2]
的值会被修改为'x'
。
输出结果:
x
总结:
- 数组定义:第二、第三和第四种方式是正确的数组定义。
- 输出结果:第二个问题的输出是
"blue"
,因为boolean
数组的默认值为false
,没有进入if
语句。 - 输出结果:第三个问题的输出是
"x"
,因为arr1
和arr2
共享同一个数组,修改arr2
也会修改arr1
。
4. 随机生成十个整数,存入数组,倒序打印,求平均值,求最大值和最大值下标
代码实现:
import java.util.Random;
public class ArrayOperations {
public static void main(String[] args) {
int[] arr = new int[10];
Random random = new Random();
// 生成随机整数并存入数组
for (int i = 0; i < 10; i++) {
arr[i] = random.nextInt(100); // 生成 0 到 99 之间的随机整数
}
// 倒序打印数组
System.out.println("倒序输出:");
for (int i = 9; i >= 0; i--) {
System.out.print(arr[i] + " ");
}
System.out.println();
// 求平均值
int sum = 0;
for (int value : arr) {
sum += value;
}
double average = sum / 10.0;
System.out.println("平均值: " + average);
// 求最大值和最大值下标
int max = arr[0];
int maxIndex = 0;
for (int i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
maxIndex = i;
}
}
System.out.println("最大值: " + max + ",下标: " + maxIndex);
}
}
解释:
- 生成 10 个随机整数存入数组。
- 通过倒序循环打印数组。
- 计算数组的平均值。
- 找到数组的最大值和对应的下标。
5. 声明
int[] x, y[]
,以下选项允许通过的是
我们来分析每个选项是否允许通过:
int[] x, y[]; // x 是一维数组,y 是二维数组
x[0] = y; // 错误:x[0] 是一个整数,y 是一个二维数组,不能赋值
y[0] = x; // 正确:y[0] 是一个一维数组,可以赋值给 x
y[0][0] = x; // 错误:y[0][0] 是一个整数,x 是一维数组,不能赋值
x[0][0] = y; // 错误:x 是一维数组,不能用 x[0][0] 访问
y[0][0] = x[0]; // 正确:y[0][0] 是一个整数,x[0] 也是一个整数,可以赋值
x = y; // 错误:x 是一维数组,y 是二维数组,不能直接赋值
允许通过的语句:
y[0] = x;
y[0][0] = x[0];
总结:
- 随机生成整数的操作可以使用
Random
类,并通过简单的循环进行倒序打印和计算最大值、平均值等。 - 声明
int[] x, y[]
的分析表明,一维数组和二维数组的操作要区分,不能直接混用。 - 十进制转十六进制的代码通过取余法实现,每一位都对应一个十六进制字符。
041. Java(10进制转16进制代码实现)
6. 十进制转十六进制
课堂案例
public class DecimalToHex {
public static void main(String[] args) {
int a = 31; // 要转换的十进制数
// 用于构建十六进制字符串
StringBuilder stringBuilder = new StringBuilder();
// 十六进制字符数组
char[] chars = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
// 通过取模和除法进行转换
while (a > 0) {
int index = a % 16; // 取 a 对 16 的余数,作为十六进制字符的下标
char temp = chars[index]; // 获取相应的十六进制字符
stringBuilder.append(temp); // 将字符追加到 StringBuilder 中
a = a / 16; // 更新 a 的值,继续处理下一位
}
// 输出结果时将字符串反转,因为低位在前
System.out.println(stringBuilder.reverse().toString());
}
}
代码实现:
public class DecimalToHex {
public static void main(String[] args) {
int d = 255; // 要转换的十进制数
StringBuffer s = new StringBuffer();
char[] b = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
// 十进制转十六进制
while (d != 0) {
s.append(b[d % 16]); // 获取当前位的16进制值
d = d / 16; // 减少位数
}
// 将结果反转并输出
String a = s.reverse().toString();
System.out.println("十六进制结果: " + a);
}
}
解释:
- 使用
StringBuffer
来存储十六进制的结果。 - 通过不断对 16 取余来获得每一位的十六进制数。
- 最后通过
reverse()
方法反转字符串,以获得正确的十六进制表示。
Hex编码
HexBin.encode
是用于将字节数组编码为十六进制字符串的方法。该方法通常用于将二进制数据表示为可读的十六进制格式。在
Java 中,它并不是 Java 标准库的一部分,但通常出现在与 XML
处理相关的库中,例如
com.sun.org.apache.xerces.internal.impl.dv.util.HexBin
。这个类用于编码和解码十六进制字符串。
假设你正在使用
HexBin.encode
,以下是如何使用该方法进行字节数组到十六进制字符串转换的示例:
使用 HexBin.encode
的示例
import com.sun.org.apache.xerces.internal.impl.dv.util.HexBin;
public class HexBinExample {
public static void main(String[] args) {
// 要转换的字节数组
byte[] byteArray = { (byte) 0x12, (byte) 0xA4, (byte) 0xF3 };
// 使用 HexBin.encode 将字节数组编码为十六进制字符串
String hexString = HexBin.encode(byteArray);
// 输出结果
System.out.println("十六进制字符串: " + hexString);
}
}
输出结果:
十六进制字符串: 12A4F3
解释:
- 字节数组:我们定义了一个包含 3 个字节的数组,分别为
0x12
、0xA4
和0xF3
。 HexBin.encode(byteArray)
:将字节数组转换为十六进制字符串表示。- 输出:结果是
12A4F3
,每个字节的值都转换为两位的十六进制数。
自己实现
HexBin.encode
的简单替代方案:
如果你不想依赖外部库,自己实现类似 HexBin.encode
的功能也非常简单。你可以使用标准 Java 来完成相同的操作:
public class CustomHexEncoder {
public static void main(String[] args) {
byte[] byteArray = { (byte) 0x12, (byte) 0xA4, (byte) 0xF3 };
String hexString = bytesToHex(byteArray);
System.out.println("十六进制字符串: " + hexString);
}
// 将字节数组转换为十六进制字符串
public static String bytesToHex(byte[] bytes) {
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
// 将字节转换为两位十六进制字符串,并拼接到结果中
String hex = String.format("%02X", b);
hexString.append(hex);
}
return hexString.toString();
}
}
输出结果:
十六进制字符串: 12A4F3
解释:
StringBuilder
:用于高效拼接字符串。String.format("%02X", b)
:将每个字节格式化为两位的十六进制大写字母。for
** 循环**:遍历字节数组,将每个字节转换为十六进制,并添加到字符串中。
append()
方法
在 Java 中,append()
方法通常用于将数据追加到可变的字符序列中。最常见的用法是使用在
StringBuilder
或 StringBuffer
类上。这两个类都是用于处理可变字符串的,允许高效地对字符串执行修改操作,如追加、插入、删除等。
append()
方法的基本功能:
作用:将各种类型的数据(字符串、字符、数值、布尔值等)追加到现有的
StringBuilder
或StringBuffer
对象末尾。常用类
:
StringBuilder
:线程不安全,但效率较高,适用于单线程环境。StringBuffer
:线程安全,适用于多线程环境,但由于同步开销较大,效率较低。
示例
1:StringBuilder.append()
使用
public class AppendExample {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("Hello");
// 追加各种数据类型
sb.append(" World"); // 追加字符串
sb.append(2024); // 追加整数
sb.append('!'); // 追加字符
sb.append(true); // 追加布尔值
// 输出结果
System.out.println(sb.toString()); // 输出 "Hello World2024!true"
}
}
解释:
append(" World")
:将字符串" World"
追加到StringBuilder
的末尾。append(2024)
:将整数2024
追加到当前字符串的末尾。append('!')
:将字符'!'
追加到末尾。append(true)
:将布尔值true
追加到末尾。- 最终的结果是:
"Hello World2024!true"
。
示例
2:StringBuffer.append()
使用
public class AppendExampleBuffer {
public static void main(String[] args) {
StringBuffer sb = new StringBuffer("The answer is ");
// 追加整数
sb.append(42);
// 输出结果
System.out.println(sb.toString()); // 输出 "The answer is 42"
}
}
解释:
- 使用
StringBuffer.append()
和StringBuilder.append()
基本相同,主要区别在于StringBuffer
是线程安全的。
常见用法:
- 追加字符串:
sb.append("abc"); // 将 "abc" 追加到现有字符串末尾
- 追加整数:
sb.append(123); // 将整数 123 追加
- 追加字符:
sb.append('a'); // 将字符 'a' 追加
- 追加布尔值:
sb.append(true); // 将布尔值 true 追加
- 追加浮点数:
sb.append(3.14); // 将浮点数 3.14 追加
链式调用:
你可以通过链式调用多次使用
append()
方法:
sb.append("Score: ").append(95).append("/100");
System.out.println(sb.toString()); // 输出 "Score: 95/100"
性能优势:
使用 StringBuilder
或 StringBuffer
的
append()
方法比使用字符串拼接(例如
"abc" + "def"
)效率更高。因为 String
是不可变的,每次拼接都会创建一个新的 String
对象,而
StringBuilder
和 StringBuffer
是可变的,不会频繁创建新对象。
总结:
append()
方法可以高效地将各种数据类型追加到StringBuilder
或StringBuffer
中。StringBuilder
适用于单线程场景,StringBuffer
适用于多线程场景。
042. Java(帮助文档的查阅)
Java API 中文文档:
该部分提供了一个链接,可以访问 Java API 的中文文档,链接为:
https://www.matools.com/
Java API 的查看方式:
该部分展示了如何查看 Java API 的结构,包括以下几个主要部分:
- 接口:Java 中的接口,用来定义类应遵循的行为规范。
- 类:Java 中的类,包含以下信息:
- 字段:类的成员变量。
- 构造方法:用于初始化类实例的构造函数。
- 方法:类中定义的各种操作或功能。
- 权限:指类或方法的访问权限修饰符,例如
public
、private
、protected
。 - 异常:描述在使用该类时可能会遇到的异常类型。
- 错误:描述类使用过程中可能发生的错误。
043. Java(类和对象)
1. 为什么需要类
类的引入是为了方便管理对象的属性和行为。图片给了一个例子:
- 三只猫:一只叫小白,2岁,白色;一只叫小黑,3岁,黑色;还有一只叫小花,4岁,花色。
- 如果有 100 只猫,管理它们的信息会变得非常复杂,而类可以帮助简化这些管理。
- 类可以作为一个模板,用于创建多个具有相似属性和行为的对象。
2. 类的定义方法
- 类是一个模板,定义了一类对象的属性和行为。
- 通过类,我们可以创建多个对象,而这些对象会共享类中定义的结构。
3. 属性(成员变量/字段/field)的概念
- 属性的定义语法
与普通变量的定义一致,只是多了访问修饰符(如
public
、private
)。 - 属性可以是任何类型,包括基本数据类型和引用类型。
- 如果属性没有被初始化,系统会赋予它默认值,和数组的行为一致。
- 例如,
int
默认值为0
,boolean
默认值为false
,引用类型默认值为null
。
- 例如,
4. 对象的创建(实例化)
- 声明:
Cat cat;
声明了一个对象引用,cat
是对象的引用变量。 - 实例化:
cat = new Cat();
使用new
关键字创建对象,并将内存地址赋给cat
。
示例:
Cat cat; // 声明
cat = new Cat(); // 实例化
5. 类与对象的概念
- 类 是对象的模板,它定义了对象的属性和方法。
- 对象 是类的实例,是实际存在的具有属性和行为的实体。
示例代码:
为了更好理解这些概念,可以用 Java 创建一个简单的 Cat
类:
class Cat {
// 属性
String name;
int age;
String color;
// 构造方法
public Cat(String name, int age, String color) {
this.name = name;
this.age = age;
this.color = color;
}
// 方法
public void meow() {
System.out.println(name + " says meow!");
}
}
public class Main {
public static void main(String[] args) {
// 创建对象
Cat cat1 = new Cat("小白", 2, "白色");
Cat cat2 = new Cat("小黑", 3, "黑色");
Cat cat3 = new Cat("小花", 4, "花色");
// 使用对象
cat1.meow(); // 输出: 小白 says meow!
cat2.meow(); // 输出: 小黑 says meow!
cat3.meow(); // 输出: 小花 says meow!
}
}
解释:
- 我们定义了一个
Cat
类,它有三个属性:name
(名字)、age
(年龄)和color
(颜色)。 - 通过构造函数创建了三只猫的对象,并调用了它们的
meow()
方法。
总结:
- 类 定义了对象的模板,包含属性和方法。
- 对象 是类的实例化,具有类中定义的属性和行为。
- 通过使用类,我们可以轻松管理多个具有相同结构的对象。
6. 类相当于自定义数据类型
- 在面向对象编程中,类相当于一种自定义的数据类型。通过定义类,我们可以创建出该类的实例,也就是对象。
- 类包含属性(字段)和方法(行为),可以对对象的属性进行赋值、获取等操作。
7. 类外面添加和获取对象属性
- 在类外部,可以通过对象的引用来访问或修改该对象的属性,通常通过“对象.属性”的形式。
- 例如:
a.age = 30;
是通过对象a
来设置其age
属性的值。
8. 构造函数的概念
- 构造函数是类的一种特殊方法,它在创建对象时自动调用,用于初始化对象的属性。
- 构造函数的名称与类名相同,它可以有参数,也可以没有参数(称为无参构造函数)。
- 例如,
Person a = new Person();
这段代码调用了Person
类的构造函数。
总结:
- 类是自定义的数据类型,用于定义对象的属性和行为。
- 对象的引用是可以共享的,多个引用可以指向同一个对象,修改一个引用的属性会影响到其他引用。
- 引用
null
表示对象不再引用任何内存地址,访问null
的属性会抛出NullPointerException
。
044. Java(对象创建过程)
9. 示例分析
Person a = new Person(); // 创建一个 Person 类的对象 a
a.age = 30; // 设置 a 的年龄为 30
a.name = "xiaojianbang"; // 设置 a 的名字为 "xiaojianbang"
Person b; // 声明一个 Person 对象 b
b = a; // b 指向 a 的内存地址,即 b 和 a 引用同一个对象
System.out.println(b.name); // 输出 b 的名字,此时会输出 "xiaojianbang"
b.age = 200; // 通过 b 修改年龄,实际上修改的是 a 的年龄
b = null; // b 不再引用 a,但 a 依然引用原对象
System.out.println(a.age); // 输出 a 的年龄,此时输出 200,因为修改影响到了 a
System.out.println(b.age); // 此行会抛出 NullPointerException,因为 b 是 null,不能访问属性
关键点:
- 对象的引用:
b = a;
表示b
和a
引用同一个Person
对象,因此修改b
的属性也会影响a
的属性。 - 空引用(
null
):当b = null;
时,b
不再引用任何对象,因此访问b.age
会抛出NullPointerException
异常。
程序运行结果:
System.out.println(b.name);
输出的是"xiaojianbang"
。System.out.println(a.age);
输出的是200
,因为b
修改了age
,而b
和a
引用的是同一个对象。System.out.println(b.age);
抛出NullPointerException
,因为b
被设置为null
,不能访问属性。
045. Java(方法的定义和调用)
课堂案例
public class Hello {
@SuppressWarnings("all")
public static void main(String[] args) {
// 调用 decToHex 方法,将不同的整数转换为十六进制
decToHex(100);
decToHex(1000);
decToHex(2000);
}
// 定义将十进制数转换为十六进制的静态方法
static void decToHex(int a) {
StringBuilder stringBuilder = new StringBuilder(); // 用于存储十六进制的结果
char[] chars = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; // 十六进制字符表
// 循环,通过除法和取余来获取每一位的十六进制数
while (a > 0) {
int index = a % 16; // 取 a 除以 16 的余数,作为十六进制数的下标
char temp = chars[index]; // 查找对应的十六进制字符
stringBuilder.append(temp); // 将字符添加到 StringBuilder 中
a = a / 16; // 更新 a 的值,进入下一个十六进制位的计算
}
// 输出反转后的结果,因为最低位先计算,需要反转字符顺序
System.out.println(stringBuilder.reverse().toString());
}
}
1. 方法的定义
- 方法的基本结构:
访问修饰符 返回值类型 方法名(形参列表) {
// 方法体
return 返回值; // 如果有返回值的话
}
- 访问修饰符:例如
public
、private
,用于控制方法的访问权限。 - 返回值类型:方法返回的数据类型,如果方法没有返回值,使用
void
。 - 方法名:方法的名称,用于调用方法。
- 形参列表:方法的参数,形参是用于接收调用方法时传递的实参。
- 方法体:方法执行的逻辑代码。
- return 语句:用于返回结果给调用者,如果返回值类型是
void
,则可以省略return
。 - 示例:
public int add(int a, int b) {
return a + b; // 返回两个数的和
}
- 方法的作用:
- 提高代码复用性:可以将常用的逻辑封装成方法,避免重复代码。
- 隐藏复杂性:调用者只需要知道如何调用方法,不需要了解方法内部实现细节。
- 参数传递和返回值:方法可以通过参数接收数据,通过返回值输出结果。可以将方法理解为加工过程,参数是输入原料,返回值是加工后的结果。
void
** 方法示例**:
public void jiao() {
// 无返回值的方法
System.out.println("This is a void method.");
}
2. 方法的调用(使用)
- 方法只有在被调用时才会执行。定义方法本身不会执行,必须通过调用来触发执行。
- 调用方法时,传递实参(如果有),方法的形参接收这些实参。
- 类外部调用方法:
- 需要创建对象,通过对象调用该类的方法。
Person person = new Person(); // 创建对象
person.jiao(); // 调用对象的方法
- 类内部调用方法:
- 可以直接在类的其他方法中调用类内定义的方法。
public class Person {
public void greet() {
sayHello(); // 直接调用类内方法
}
private void sayHello() {
System.out.println("Hello!");
}
}
3. 方法的特点:
- 方法是类中的一段独立的小程序,具有特定的功能。
- 方法可以接收参数,并返回结果。
- 通过定义方法,可以将复杂的逻辑封装起来,提高代码的可读性和复用性。
046. Java(方法的返回值和参数)
3. 返回值
- 定义:一个方法只能返回一个值,如果需要返回多个值,可以使用数组或对象。
- 返回值类型:返回值可以是任意类型,包括基本数据类型和引用类型(例如对象、数组等)。
return
关键字:返回值的类型必须和return
语句的值类型一致或兼容。void
返回类型:如果方法没有返回值,返回类型为void
,此时return
语句可以省略(但如果明确写了return
,不能返回任何值)。
示例:
public int sum(int a, int b) {
return a + b; // 返回两个数的和
}
public void printMessage() {
System.out.println("Hello, World!"); // 没有返回值
}
sum
方法返回一个int
类型的值,printMessage
方法没有返回值(void
类型)。
4. 参数
- 方法参数:方法可以有零个或多个参数,参数类型可以是任意类型(包括基本类型和引用类型)。
- 形参和实参:
- 形参(形式参数):方法定义时的参数,称为形参。
- 实参(实际参数):调用方法时传递的参数,称为实参。
- 实参的类型、个数和顺序必须与形参匹配。
示例:
public void greet(String name) {
System.out.println("Hello, " + name);
}
- 调用时,
greet("Alice")
中"Alice"
是实参,name
是形参。
5. 方法体
- 方法体:是定义在方法中的代码块,包含完成功能的具体代码。
- 语句类型:方法体中可以包含各种语句,如输出语句、变量声明、运算符、分支语句(
if
、switch
)、循环语句(for
、while
)等。 - 方法调用:方法体中可以调用其他方法,但不能在方法内部定义方法。
示例:
public void displaySum(int a, int b) {
int sum = a + b; // 运算
System.out.println("Sum: " + sum); // 输出结果
}
小结:
- 返回值 决定了方法的输出类型,
void
表示无返回值。 - 参数 允许方法接收外部传入的数据,形参和实参必须匹配。
- 方法体 包含具体的实现逻辑,是方法的核心。
047. Java(方法传参机制)
1. 方法嵌套调用执行顺序
- 当方法被调用时,系统会为该方法分配一个独立的 栈空间(即栈帧)。在这个栈空间中,存放方法的局部变量、参数等数据。
- 方法执行结束后,或者执行到
return
语句时,栈帧会被销毁,程序返回到调用方法的位置,继续执行调用方法后面的代码。 - 对于
main
方法,当它执行完毕后,整个程序就会结束。
示例:
public class Example {
public static void main(String[] args) {
methodA(); // 调用 methodA
System.out.println("Main finished.");
}
public static void methodA() {
System.out.println("In methodA");
methodB(); // 调用 methodB
}
public static void methodB() {
System.out.println("In methodB");
}
}
执行顺序:
main
方法调用methodA
,methodA
再调用methodB
。- 当
methodB
执行完毕后,返回到methodA
,然后返回到main
,最后程序结束。
2. 值传递 / 值拷贝
值传递:当方法的参数是基本数据类型时,传递的是该值的 拷贝。因此,形参的修改不会影响到实参。
例如:
public static void changeValue(int num) {
num = 100; // 修改的是值的副本
}
public static void main(String[] args) {
int a = 50;
changeValue(a); // 实参 a 不会受到影响
System.out.println(a); // 输出仍为 50
}
3. 引用传递
引用传递:当方法的参数是引用类型(如数组、对象)时,传递的是该对象的 地址。因此,方法内部对形参的修改可能会影响到实参。
例如:
public static void changeArray(int[] arr) {
arr[0] = 100; // 修改数组中的值
}
public static void main(String[] args) {
int[] array = {1, 2, 3};
changeArray(array); // 传递的是数组的引用
System.out.println(array[0]); // 输出 100,因为数组已被修改
}
- 由于传递的是引用类型的地址,因此形参和实参指向的是同一个内存地址。
4. String 的传递
String 的特殊性:在 Java 中,
String
是一个不可变对象(immutable object)。虽然String
是引用类型,但在方法中对String
的修改实际上是创建了一个新的String
对象,而不会改变原来的String
。null
** 与重新赋值**:当将String
赋值为null
或赋值为一个新的值时,引用会指向一个新的内存地址,而原来的String
对象不会被修改。例如:
public static void modifyString(String str) {
str = "New String"; // 修改的是 str 的引用,不影响外部的变量
}
public static void main(String[] args) {
String original = "Original";
modifyString(original);
System.out.println(original); // 仍然输出 "Original"
}
String
的这种行为看起来像是 值传递,但实际上它仍然是引用传递,只是由于String
的不可变特性,使得外部的值不被修改。
小结:
- 方法调用时,会分配一个独立的栈空间,执行完毕后返回调用处继续执行。
- 基本数据类型是 值传递,形参的修改不影响实参。
- 引用类型是 引用传递,形参的修改可能影响到实参。
String
虽然是引用类型,但由于不可变性,传递后在方法内部的修改不会影响原始的String
对象。
048. Java(static)
1. 静态属性(类属性)与实例属性(对象属性)
- 静态属性(类属性):
- 静态属性属于类,而不属于某个具体的对象。
- 静态属性在内存中只有一份,所有该类的对象共享这一个静态属性。
- 访问方式:可以通过 类名.属性名 或者 对象.属性名 来访问静态属性,推荐使用类名访问。
public class Example {
static int staticVar = 10; // 静态属性
}
// 访问静态属性
int value = Example.staticVar; // 推荐通过类名访问
- 实例属性(对象属性):
- 实例属性属于每个具体的对象,每个对象都有一份独立的实例属性。
- 访问方式:只能通过对象来访问实例属性,不能通过类名访问。
public class Example {
int instanceVar = 20; // 实例属性
}
// 创建对象并访问实例属性
Example obj = new Example();
int value = obj.instanceVar; // 通过对象访问实例属性
2. 静态方法与实例方法
- 静态方法:
- 静态方法属于类,而不是对象。
- 静态方法可以通过 类名 或 对象 来调用,推荐使用类名调用。
- 静态方法不能访问实例属性或调用实例方法,因为静态方法不依赖于任何对象。
public class Example {
static void staticMethod() {
System.out.println("This is a static method.");
}
}
// 调用静态方法
Example.staticMethod(); // 推荐通过类名调用
- 实例方法:
- 实例方法属于对象,必须通过对象来调用。
- 实例方法可以访问类的静态属性,也可以访问实例属性。
public class Example {
int instanceVar = 10;
void instanceMethod() {
System.out.println("This is an instance method. Instance var: " + instanceVar);
}
}
// 创建对象并调用实例方法
Example obj = new Example();
obj.instanceMethod(); // 通过对象调用实例方法
3. 补全 method 方法嵌套调用
图片给出的嵌套调用示例:
System.out.println(method(method(method(100.2, 1.0), 1.0), 100));
- 该行代码展示了一个方法的多重嵌套调用。方法
method
的返回值再次作为参数传入下一层的method
调用。 - 为了补全该代码的逻辑,我们可以假设
method
方法是一个简单的数学计算方法,如加法或乘法。
例如:
public class Example {
public static double method(double a, double b) {
return a + b; // 例如简单返回 a 和 b 的和
}
public static void main(String[] args) {
System.out.println(method(method(method(100.2, 1.0), 1.0), 100));
}
}
执行顺序:
- 首先计算
method(100.2, 1.0)
,假设返回结果为101.2
。 - 然后计算
method(101.2, 1.0)
,结果为102.2
。 - 最后计算
method(102.2, 100)
,最终结果为202.2
。
总结:
- 静态属性和方法 是类级别的,所有对象共享,可以通过类名访问。
- 实例属性和方法 是对象级别的,每个对象有自己独立的实例属性和方法,必须通过对象来访问。
- 方法可以嵌套调用,方法的返回值可以作为参数传递给另一个方法。
049. Java(练习1)
1. 编写一个
Utils
类,定义 getMax
方法
该方法接收一个 double
数组,找出数组中的最大值并返回。如果数组为空或为
null
,需要处理异常。以下是实现代码:
public class Utils {
public static double getMax(double[] arr) {
// 检查数组是否为 null 或长度为 0
if (arr == null || arr.length == 0) {
throw new IllegalArgumentException("数组不能为空或为 null");
}
double max = arr[0];
for (double num : arr) {
if (num > max) {
max = num;
}
}
return max;
}
public static void main(String[] args) {
double[] numbers = {1.5, 2.8, 3.9, 0.5};
System.out.println("最大值: " + getMax(numbers));
}
}
- 异常处理:当数组为
null
或空时,抛出IllegalArgumentException
异常。 - 逻辑:遍历数组,找出最大值。
在 Java 中,包装类 和 自定义类 是两个不同的概念,涉及到基础数据类型和面向对象编程。下面将分别解释它们。
1. 包装类(Wrapper Classes)
Java
是一种面向对象的编程语言,但它有八种基本数据类型,即
int
、char
、double
等,这些数据类型不是对象。因此,Java
提供了包装类(Wrapper
Classes)来将这些基本数据类型转换为对象。包装类为每种基本数据类型提供了一个与之对应的类,从而允许基本类型也可以被当作对象来处理。
常见的包装类:
int
对应的包装类是Integer
double
对应的包装类是Double
char
对应的包装类是Character
boolean
对应的包装类是Boolean
float
对应的包装类是Float
long
对应的包装类是Long
short
对应的包装类是Short
byte
对应的包装类是Byte
为什么需要包装类?
- 对象化操作:Java 集合类如
ArrayList
、HashSet
只能存储对象,不能存储基本数据类型。因此需要包装类来把基本类型转换为对象。 - 便捷功能:包装类提供了许多有用的方法,如
Integer.parseInt()
,用于将字符串转换为int
。 - 自动装箱与拆箱:Java 允许基本类型和包装类之间的自动转换,这被称为自动装箱(autoboxing)和自动拆箱(unboxing)。
示例:
public class WrapperExample {
public static void main(String[] args) {
// 基本类型 int
int primitiveInt = 5;
// 包装类型 Integer
Integer wrappedInt = primitiveInt; // 自动装箱
// 基本类型和包装类型之间的转换
int anotherInt = wrappedInt; // 自动拆箱
System.out.println("包装类型: " + wrappedInt); // 输出 5
System.out.println("基本类型: " + anotherInt); // 输出 5
}
}
自动装箱与拆箱:
- 装箱:将基本类型转换为包装类对象。
- 拆箱:将包装类对象转换为基本类型。
例如:
Integer wrappedInt = 5; // 自动装箱
int primitiveInt = wrappedInt; // 自动拆箱
2. 自定义类(Custom Classes)
自定义类 是开发者根据业务逻辑或需求自己定义的类,是
Java 面向对象编程的基础。Java 提供了大量内置的类,如
String
、Integer
等,但当这些内置类不能满足需求时,开发者可以根据实际问题定义自己的类。通过定义自定义类,开发者可以创建具有自己特定属性和方法的对象。
自定义类的特征:
- 自定义类可以包含属性(成员变量)和方法。
- 自定义类可以通过构造函数来创建对象。
- 自定义类可以继承、实现接口、重写方法等。
自定义类示例:
// 自定义一个类 Person
public class Person {
// 属性(成员变量)
private String name;
private int age;
// 构造方法,用于初始化对象
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 方法,用于获取姓名
public String getName() {
return name;
}
// 方法,用于获取年龄
public int getAge() {
return age;
}
// 方法,用于显示个人信息
public void displayInfo() {
System.out.println("姓名: " + name + ", 年龄: " + age);
}
// 主方法
public static void main(String[] args) {
// 创建自定义类的对象
Person person = new Person("Alice", 25);
// 调用方法
person.displayInfo(); // 输出: 姓名: Alice, 年龄: 25
}
}
自定义类的作用:
- 抽象与建模:自定义类用于表示现实世界中的对象。通过定义类,开发者可以将事物的属性和行为抽象为程序中的类和方法。
- 复用性:自定义类可以在程序中复用,不同的对象可以共享同一个类定义。
常见的自定义类使用场景:
- 定义实体类(如
Student
、Employee
)表示实际事物。 - 定义工具类(如
MathUtils
)封装某些功能。 - 定义服务类(如
UserService
)用于处理业务逻辑。
区别:
特性 | 包装类(Wrapper Classes) | 自定义类(Custom Classes) |
---|---|---|
作用 | 将基本类型包装成对象 | 根据需求创建包含属性和行为的类 |
使用场景 | 需要将基本类型转换为对象时使用 | 当内置类不满足需求时,开发者定义类 |
例子 | Integer , Double ,
Character |
Person , Car , Employee |
内置/自定义 | Java 内置 | 开发者根据需求自定义 |
是否包含方法 | 包含常用的方法(如 parseInt ) |
根据需要定义方法 |
目的 | 提供基本类型的对象化支持 | 根据业务需求建模和实现特定逻辑 |
总结:
- 包装类 是 Java 提供的类,用于将基本数据类型封装成对象,使基本数据类型可以作为对象处理,同时提供了许多方便的方法。
- 自定义类 是开发者根据需求和业务逻辑创建的类,用于表示对象的属性和行为,便于实现具体的功能和逻辑。
050. Java(练习2)
2. 定义 find
方法
该方法用于在字符串数组中查找元素,找到则返回索引,找不到则返回
-1
。实现如下:
public class Utils {
public static int find(String[] arr, String target) {
if (arr == null) {
throw new IllegalArgumentException("数组不能为空");
}
for (int i = 0; i < arr.length; i++) {
if (arr[i].equals(target)) {
return i;
}
}
return -1; // 如果没找到,返回 -1
}
public static void main(String[] args) {
String[] names = {"Alice", "Bob", "Charlie"};
System.out.println("Bob 的索引: " + find(names, "Bob"));
System.out.println("Eve 的索引: " + find(names, "Eve"));
}
}
- equals 方法:用于比较字符串内容。
- 逻辑:遍历数组,比较字符串并返回索引。
3. 定义 Circle
类
定义一个 Circle
类,包含属性半径,并提供方法显示圆周长和面积。实现如下:
public class Circle {
private double radius; // 圆的半径
public Circle(double radius) {
this.radius = radius;
}
// 计算并返回圆的周长
public double getCircumference() {
return 2 * Math.PI * radius;
}
// 计算并返回圆的面积
public double getArea() {
return Math.PI * radius * radius;
}
public static void main(String[] args) {
Circle circle = new Circle(5.0);
System.out.println("圆的周长: " + circle.getCircumference());
System.out.println("圆的面积: " + circle.getArea());
}
}
- 周长公式:
。 - 面积公式:
。
4. 分析代码输出结果
public class Hello {
int count = 100;
public void count1() {
count = 200;
System.out.println("count1=" + count);
}
public void count2() {
System.out.println("count2=" + count++);
}
public static void main(String[] args) {
new Hello().count1(); // 匿名对象,只能使用一次
Hello t2 = new Hello();
t2.count2();
t2.count2();
}
}
代码运行顺序:
new Hello().count1();
- 创建一个匿名的
Hello
对象,并调用count1()
方法。 - 输出:
count1=200
。 - 注意:匿名对象只能调用一次,之后不再使用。
- 创建一个匿名的
Hello t2 = new Hello();
- 创建一个新的
Hello
对象t2
,其count
属性的初始值为100
。
- 创建一个新的
t2.count2();
- 调用
count2()
方法,打印count
,此时count
为100
,并且count++
使count
递增为101
。 - 输出:
count2=100
。
- 调用
t2.count2();
- 再次调用
count2()
方法,count
的值已经变为101
,并且再次递增为102
。 - 输出:
count2=101
。
- 再次调用
最终输出结果:
count1=200
count2=100
count2=101
让我们逐步分析这段代码,以确定其输出结果。
实例变量和静态变量
如果 count
变量没有被声明为
static
,也就是变成了一个实例变量,而不是静态变量,那么程序的行为会有所不同。让我们分析一下这种情况。
1. 当前代码中的
count
是 static
的含义
在当前的代码中,count
是一个静态变量,这意味着它是属于整个类
Hello
的,而不是某个具体对象。所有 Hello
类的实例共享这一个 count
变量,修改这个变量会影响到所有对象。
2. 如果去掉
static
,让 count
变成实例变量
如果我们把 count
变成一个实例变量,去掉
static
关键字,那么每个 Hello
类的对象都会有自己独立的 count
值,互不干扰。实例变量是对象的属性,属于每个具体对象,不再是整个类共享的变量。
修改后的代码如下:
public class Hello {
int count = 100; // 实例变量,不再是静态变量
public void count1() {
count = 200;
System.out.println("count1=" + count);
}
public void count2() {
System.out.println("count2=" + count++);
}
public static void main(String[] args) {
new Hello().count1(); // 匿名对象,只能使用一次
Hello t2 = new Hello();
t2.count2();
t2.count2();
}
}
3. 分析去掉
static
后的变化
1. new Hello().count1();
创建了一个匿名的
Hello
对象,并调用count1()
方法。因为
count
** 现在是实例变量**,所以这个对象有自己独立的count
变量,初始值为100
。调用
count1()
后,将该对象的count
设置为200
,并打印count1=200
。但是这个
count
只影响当前对象,其他对象的count
不受影响。输出:
count1=200
2. Hello t2 = new Hello();
- 创建了另一个
Hello
对象t2
。 - 因为
count
是实例变量,t2
对象有自己独立的count
变量,初始值仍然是100
(每个对象的实例变量在对象创建时会初始化)。
3. t2.count2();
调用
t2
对象的count2()
方法。t2
的count
值此时是100
,调用count2()
打印当前count
的值并自增。打印
count2=100
,然后count
递增为101
。输出:
count2=100
4. t2.count2();
再次调用
t2.count2()
,此时count
已经变为101
,调用count2()
将先打印当前count
,再自增。打印
count2=101
,然后count
递增为102
。输出:
count2=101
4. 最终输出结果
当 count
是实例变量时,最终的输出结果是:
count1=200
count2=100
count2=101
5. 区别总结
- 静态变量(
static
):类的所有实例共享同一个静态变量,修改静态变量会影响所有实例。在原始代码中,count
是静态的,所以所有对象共享一个count
,因此无论哪个对象修改了count
,其他对象都能看到这个变化。 - 实例变量(无
static
):每个对象都有自己独立的实例变量,修改一个对象的实例变量不会影响其他对象的实例变量。在去掉static
后,count
成为实例变量,因此每个对象都有自己独立的count
值,互不影响。
总结:
- 如果
count
是静态变量,所有对象共享一个值,修改它会影响所有实例。 - 如果
count
是实例变量,每个对象有自己独立的count
值,修改一个对象的count
不会影响其他对象。
Java中的函数与方法
在 Java 中,count1()
是一个方法,而不是一个“函数”。尽管在很多编程语言(尤其是像 C 或
Python 这样的语言)中,“函数”和“方法”有时可以互换使用,但在 Java
中,它们有不同的含义。
方法与函数的区别:
方法:方法是属于某个类或对象的函数。在 Java 中,所有的函数都必须定义在类中,因此 Java 中的函数实际上是类的成员方法。
- 方法可以访问类的成员变量(如
count
)并且可能与类的状态相关联(即它们可以修改类的属性或依赖于类的属性来进行操作)。 - 例如:
count1()
是Hello
类的一个方法,它可以直接访问和修改类的静态变量count
。
- 方法可以访问类的成员变量(如
函数:通常指的是不依赖于类或对象的独立可执行代码块。在 Java 中没有独立的函数,因为所有的行为都必须通过类或对象来定义和调用。
在其他语言中,函数可以独立于对象或类存在。例如在 C 语言中,你可以直接定义一个函数:
int add(int a, int b) {
return a + b;
}
但在 Java 中,每个“函数”必须放在类内部,因此它被称为**方法**。
count1()
是什么?
public void count1() {
count = 200;
System.out.println("count1=" + count);
}
count1()
是一个方法:- 它是
Hello
类的一个实例方法,定义在类Hello
内部。 - 该方法没有返回值(
void
表示它不返回任何值)。 - 它修改了静态变量
count
并输出count
的值。
- 它是
方法的组成部分:
- 访问修饰符:
public
- 表示该方法可以从类外部访问。
- 返回类型:
void
- 表示该方法没有返回值。
- 方法名称:
count1
- 是该方法的名称,用于调用此方法。
- 参数列表:
()
- 这里没有参数,因此括号中为空。
- 方法体:
{ ... }
- 包含方法执行的代码逻辑。在这个例子中,它将静态变量
count
设置为200
并打印。
- 包含方法执行的代码逻辑。在这个例子中,它将静态变量
总结:
count1()
是一个方法,而不是函数,因为它必须定义在类中,并且与类的成员变量(count
)相关联。- Java 中没有独立的函数,所有的行为都必须定义为类的方法。
051. Java(重载)
1. 重载的概念:
在 Java 中,方法重载(Overloading) 是指在同一个类中可以定义多个同名方法,只要它们的参数列表不同(即参数的数量或类型不同),编译器就可以区分这些方法。这意味着即使方法名称相同,Java 也允许通过不同的参数组合来调用不同的实现。
- 参数列表不同:
- 参数的数量不同。
- 参数的类型不同(即使参数的数量相同)。
- 与返回值类型无关:重载只与方法的参数列表有关,方法的返回值类型不能用来区分重载的方法。也就是说,仅改变返回类型是不能实现重载的。
2. 练习部分分析:
基础方法定义为:
void show(int a, char b, double c) {}
- 这是一个名为
show
的方法,参数列表为int a, char b, double c
。我们需要判断哪些方法可以作为这个方法的重载。
a.
void show(int x, char y, double z) {}
- 该方法的参数列表为
int, char, double
,与基础方法的参数列表完全相同。 - 结果:不能构成重载,因为参数列表相同。
b.
int show(int a, double c, char b) {}
- 该方法的参数顺序不同,参数列表为
int, double, char
,与基础方法的int, char, double
不同。 - 尽管返回值类型不同(
int
),但重载不考虑返回值类型,只看参数列表。 - 结果:可以构成重载,因为参数类型的顺序不同。
c.
void show(int a, double c, char b) {}
- 该方法与选项 b 的区别仅在于返回值类型。此方法的参数列表为
int, double, char
,与基础方法的int, char, double
不同。 - 结果:可以构成重载,因为参数类型的顺序不同。
d.
boolean show(int c, char b) {}
- 该方法的参数列表为
int, char
,参数的数量少于基础方法的参数列表(只有两个参数)。 - 结果:可以构成重载,因为参数数量不同。
e.
void show(double c) {}
- 该方法只有一个参数
double
,与基础方法的三个参数完全不同。 - 结果:可以构成重载,因为参数数量和类型不同。
f.
double shows(int x, char y, double z) {}
- 该方法的名称是
shows
,而不是show
。重载的前提是方法名称相同,因此这是一个完全不同的方法。 - 结果:不能构成重载,因为方法名称不同。
总结:
- 可以构成重载的方法:b、c、d、e。
- 不能构成重载的方法:a、f。
052. Java(可变参数)
1. 为什么需要可变参数
可变参数(Variadic Parameters)允许你在定义函数时不限定参数的数量,这在处理可变数量的输入时非常有用。例如,计算多个数字的和时,我们不确定到底有多少个数字传入,因此使用可变参数可以让函数适应不同数量的参数。
2. 可变参数的使用
可变参数的使用通常通过“省略号”符号(...
)来表示。在C语言、C++、Java、Python等语言中都可以使用类似的机制。以下是一个Java中的例子:
int sum(int... nums) {
int result = 0;
for (int num : nums) {
result += num;
}
return result;
}
- 可变参数可以当作数组处理:在函数内部,可变参数可以被视作一个数组,函数可以通过遍历数组来处理所有传入的参数。
- 可变参数的实参可以为0个或任意多个:调用时,可以传递任何数量的参数,包括0个,这让函数更加灵活。
- 可变参数的本质是数组:在底层实现上,可变参数实际上就是一个数组。虽然语法上可以看作是一组分开的参数,但在函数内会被处理为数组。
- 可变参数可以和普通类型的参数一起使用:在函数定义中,可变参数可以与其他普通参数同时存在,但必须保证可变参数位于参数列表的最后。例如:
void printDetails(String name, int... scores) {
System.out.println("Name: " + name);
for (int score : scores) {
System.out.println("Score: " + score);
}
}
注意事项:
- 在一个形参列表中只能出现一个可变参数,且必须放在最后。例如,以下写法是不合法的:
void example(int... nums, String... names) {} // 错误
正确的方式是将所有可变参数放在参数列表的最后。
总结
可变参数提供了极大的灵活性,使得函数可以处理任意数量的输入,而不需要明确指定参数的数量。在实现上,它们与数组紧密相关,这使得它们在实现灵活性和效率之间达成了平衡。
053. Java(构造器)
1. 构造器的特点
- 构造器的名称和类名一致,且没有返回值。
- 当对象创建时,会自动调用对应的构造器。
- 如果没有定义构造器,编译器会自动生成一个默认的无参构造器(也叫“默认构造器”)。
- 一个类可以定义多个构造器,这就是构造器重载。
- 一旦定义了自己的构造器(包括带参构造器),编译器不会再自动生成默认的无参构造器,除非你显式定义一个无参构造器。
2. 对象创建流程
以下是对象创建流程的详细解析:
class Person {
int age = 90;
String name;
Person(String n, int a) {
name = n;
age = a;
}
}
Person p = new Person("xiaojianbang", 31);
对象创建流程:
- 加载类信息:当程序首次创建
Person
类对象时,Java虚拟机会加载Person
类的字节码,类信息只会加载一次。 - 分配内存:在堆中为对象分配内存空间,这个内存区域用于存储对象的实例变量。
- 默认初始化:分配的内存区域会先进行默认初始化,例如,
int
类型默认初始化为0,String
类型默认初始化为null
,此时age = 0
,name = null
。 - 显式初始化:根据类中的显式初始化语句,将实例变量赋予指定的初始值,例如在类中定义的
age = 90
。 - 构造器初始化:调用构造器,执行构造器中的语句,将传入的参数赋值给实例变量,如
name = "xiaojianbang"
,age = 31
。 - 返回对象引用:对象的引用(在堆中对象的地址)赋值给引用变量
p
。此时,p
指向在堆中的Person
对象。
对象的初始化顺序:
- 加载类信息。
- 内存分配:在堆上为对象分配空间。
- 默认初始化:成员变量的默认值(
int
类型为0,String
类型为null)。 - 显式初始化:成员变量的显式初始化语句,如
int age = 90;
。 - 构造器初始化:根据传入的构造器参数,进行初始化。
这样,在对象p
创建后,它的属性会是age = 31
,name = "xiaojianbang"
。
054. Java(变量作用域)
1. 全局变量
- 定义:全局变量通常是类的成员变量(属性变量),它们可以在类的任何地方被访问,甚至被其他类访问(如果访问修饰符允许)。
- 访问修饰符:全局变量可以使用
public
、private
、protected
等访问修饰符来限制其访问权限。 - 默认初始化:全局变量在声明时,如果没有显式赋值,系统会为其赋予默认值(如
int
类型为0,boolean
类型为false
,引用类型为null
)。 - 生命周期:全局变量的生命周期随着对象的创建而开始,随着对象的销毁而结束。也就是说,全局变量的生命周期较长,通常伴随着对象的存在。
2. 局部变量
- 定义:局部变量是在方法或构造函数内部声明的变量。它们只能在声明它们的代码块中使用,出了这个代码块之后就无法访问。
- 无访问修饰符:局部变量不能使用访问修饰符(如
public
、private
等),因为它们的作用域局限于方法或代码块内。 - 无默认值:局部变量必须显式初始化后才能使用。如果未赋值,编译器会报错。
- 生命周期:局部变量的生命周期较短,从代码块开始执行时创建,结束时销毁。
3. 块级作用域
- 定义:块级作用域是指在代码块中声明的变量,其作用范围仅限于这个代码块内部。例如,在
if
、for
、while
等控制结构中声明的变量只在对应的代码块中可见。 - 静态代码块与方法代码块:在Java中,静态代码块、构造代码块、方法代码块也存在块级作用域。每个块的变量只能在相应的块中使用。当类加载的时候静态代码块最先执行,因为类只加载一次,所以静态代码块只执行一次。
4. 属性和局部变量的重名
- 重名问题:在同一个作用域中,属性变量和局部变量可以重名。在这种情况下,局部变量优先。即在方法内部,若局部变量和类的成员变量同名,局部变量会遮蔽全局的成员变量。
- 解决办法:可以通过
this
关键字来区分同名的成员变量和局部变量。例如:
class Person {
String name;
Person(String name) {
this.name = name; // 这里的this.name指代全局变量name,右边的name指代局部变量
}
}
- 局部变量不能重名:在同一作用域中,局部变量之间不能重名,否则编译器会报错。
总结
变量的作用域是指变量在程序中可见和可用的范围。全局变量的作用域较大,可以在类的任意位置访问,而局部变量的作用域则限于声明它们的代码块中。在实际编程中,需要合理管理变量的作用域,以避免命名冲突和资源浪费。
055. Java(this)
1. 什么是 this
this
是 Java 中的一个关键字,它指代当前对象。Java
虚拟机会给每个对象分配一个 this
引用,用来表示当前对象。
- 当我们在类的方法中使用
this
时,它表示调用该方法的那个对象。例如:
public class Person {
String name;
Person(String name) {
this.name = name; // this指代当前的对象,避免局部变量和实例变量的混淆
}
void printHashCode() {
System.out.println(this.hashCode()); // 打印当前对象的hashCode
}
}
Person p = new Person("Tom");
p.printHashCode();
2. this
用于区分当前对象的属性和局部变量
当构造函数或者方法中的参数名称与成员变量名称相同时,可以使用
this
来区分成员变量和局部变量。例如:
class Person {
String name;
Person(String name) {
this.name = name; // this.name指代成员变量,name指代局部变量
}
}
在上述例子中,this.name
表示当前对象的 name
属性,而右侧的 name
是方法参数。
3.
this
不能在类外部使用,只能在类内部方法中使用
this
只能在类的实例方法和构造方法中使用,用于指代当前对象。在类外部无法使用
this
。例如,不能在静态方法中使用
this
,因为静态方法属于类而不是具体的实例。
4. 类内部赋值和获取实例属性
this
可以用于在类的实例方法中访问和操作实例变量。例如,在方法中通过
this
来获取或修改当前对象的属性值:
class Person {
String name;
void setName(String name) {
this.name = name; // 使用this来操作当前对象的属性
}
String getName() {
return this.name; // 返回当前对象的name属性
}
}
5. 类内部调用方法
在类的内部,一个方法可以通过 this
调用当前对象的其他方法。例如:
class Person {
void method1() {
System.out.println("Method 1");
}
void method2() {
this.method1(); // 使用this调用当前对象的method1
}
}
注意:在类内部调用方法时,可以省略
this
,但在某些情况下,为了更清晰的表达,可以显式地使用
this
。
6. this
调用构造方法
在构造函数中,this(参数列表)
可以用于调用该类的其他构造函数(构造方法的重载),这称为构造方法的链式调用。使用
this()
调用构造方法必须是构造函数中的第一条语句。例如:
class Person {
String name;
int age;
Person() {
this("Unknown", 0); // 调用带参数的构造方法
}
Person(String name, int age) {
this.name = name;
this.age = age;
}
}
这里无参构造函数通过 this("Unknown", 0)
调用了带参构造函数。
总结
this
关键字用于指代当前对象,解决局部变量和成员变量同名时的冲突。this
只能在类的实例方法和构造函数中使用,不能在类外部或静态方法中使用。this
可以用于类内部方法之间的调用,以及构造函数的链式调用。
056. Java(包)
1. 包的三大作用
包(Package)在 Java 中有以下三个主要作用:
- 区分相同名字的类:在大规模项目中,不同开发者可能会创建名称相同的类。通过包,Java 允许这些类存在于不同的命名空间下,而不会发生冲突。
- 管理类:当项目中包含很多类时,可以通过包将相关的类组织在一起,从而提供更好的管理和结构。
- 控制访问范围:包可以通过访问修饰符(如
public
、protected
、无修饰符等)来控制类的可见性,从而提高代码的封装性和安全性。
2. 建包
在 Java 中,包实际上是通过在文件系统中创建不同的文件夹/目录来组织类文件。包名与目录结构相对应。例如:
src/com/xiaojianbang/app/Person.java
在代码中,Person
类属于
com.xiaojianbang.app
包,目录结构与包名严格对应。
3. 打包
在 Java 文件中,使用 package
关键字来声明该类所属的包。通常,这条语句需要放在 Java
文件的第一行,并且每个 Java 文件只能声明一个包。例如:
package com.xiaojianbang.app;
命名规则
- 包名通常全部小写字母。
- 包名使用点(
.
)来分隔不同的层级。 - 包名遵循与变量名类似的命名规则,不能包含空格和特殊字符。
4. 导包
为了使用其他包中的类,需要使用 import
关键字将其导入。在类的定义之前可以有多个 import
语句,且没有顺序要求。导入时可以导入整个包,也可以导入特定的类。例如:
import java.util.*; // 导入java.util包下的所有类
import java.util.HashMap; // 仅导入HashMap类
自动导入
java.lang
包是 Java
的基础包,系统会默认导入该包下的所有类,因此在代码中不需要显式导入。
总结
- 包用于组织和管理类,避免命名冲突,并可以控制类的访问权限。
- 通过
package
声明类所属的包,包名与文件系统目录相对应。 import
用于导入其他包中的类,可以导入整个包或者指定某个类。
057. Java(访问修饰符)
访问修饰符的作用
Java 中的访问修饰符用于控制类、属性和方法的访问权限,它们决定了哪些类或对象可以访问特定的类成员(属性、方法等)。主要有四种访问修饰符:
- public:公共访问修饰符,表示可以被所有类访问。
- protected:受保护的修饰符,表示可以被同一个包中的类及其子类访问。
- 无修饰符(default):包访问权限,表示只能被同一个包中的类访问。
- private:私有访问修饰符,表示只能被同一个类中的方法或属性访问。
表格解释
表格中展示了不同访问修饰符在本类、同包、子类和不同包中的访问权限:
访问修饰符 | 本类 | 同包 | 子类 | 不同包 |
---|---|---|---|---|
public | ✓ | ✓ | ✓ | ✓ |
protected | ✓ | ✓ | ✓ | ✗ |
无修饰符 | ✓ | ✓ | ✗ | ✗ |
private | ✓ | ✗ | ✗ | ✗ |
- public:可以在任何地方访问,无论是同包还是不同包的类都可以访问。
- protected:可以在同包中访问,并且允许不同包中的子类访问。
- 无修饰符(包级访问):只能在同一个包内访问,无法被子类或不同包中的类访问。
- private:只能在本类中访问,其他类甚至同包中的类和子类都不能访问。
关键点解释
- 访问修饰符用于控制属性和方法的访问权限:访问修饰符决定了类成员(如属性和方法)的可见性和访问范围,以确保类的封装性和安全性。
- 修饰符可以用来修饰类中的属性、成员方法以及类:访问修饰符不仅可以修饰类中的变量和方法,还可以修饰类本身。需要注意的是,只有
public
和默认修饰符(即无修饰符)可以用于类的修饰,private
和protected
不能用于类的修饰。 - 只有
public
** 和默认修饰符才能修饰类**:类只能使用public
或者默认(包级访问)权限修饰符进行修饰。private
和protected
修饰符只能用于类的成员,不能用于修饰类本身。
总结
访问修饰符的使用能够帮助我们在开发中合理地管理类的访问权限,确保类的内部实现细节被封装起来,暴露出需要提供给外部使用的接口。不同修饰符的使用场景也有所不同:
- 使用
public
来允许全局访问。 - 使用
protected
来允许子类或包内的类访问。 - 使用默认修饰符来限制访问范围仅限于同包。
- 使用
private
来确保类的内部实现不被外部访问。
058. Java(封装)
1. 封装的概念
封装(Encapsulation)是面向对象编程的四大基本原则之一,它的主要思想是隐藏对象的属性和实现细节,仅对外部提供操作对象数据的接口,控制程序中属性的读和写的访问权限。
- 封装通过将类的成员变量(属性)设置为
private
,使得外部无法直接访问这些变量,防止不合适的访问或修改。 - 对外提供
public
的方法(通常是get
和set
方法),用于获取和修改这些私有属性,并可以通过这些方法进行数据的验证和控制。
举例:当你使用一台电脑时,你可以操作它,但你不需要也无法知道电脑内部具体是如何运行的。这就是封装的概念,隐藏了内部的实现细节。
2. 封装的好处
- 隐藏实现细节:调用者只需要知道传递什么参数,会产生什么效果,返回什么数据,而不需要了解内部的实现原理。
- 数据验证:在设置属性值时,可以通过
set
方法对传入的数据进行合理性验证,防止不合法的数据被设置。 - 提高代码可维护性:封装后的代码具有更好的可扩展性和维护性,修改类的实现不会影响到外部的调用者。
通过将属性设为 private
,并通过 get
和
set
方法来访问这些属性,开发者可以灵活地对数据进行验证或处理,保证数据的正确性和安全性。
3. 练习题
定义一个 Account
类,要求具有以下属性:
name
:表示用户名,长度应为2到4个字符,且初始化后不能修改。balance
:表示余额,必须大于20。password
:表示密码,必须为6位数字。
如果属性的值不符合要求,应该给出提示信息,并设定合理的默认值。
代码实现示例:
public class Account {
private String name;
private double balance;
private String password;
// 构造方法
public Account(String name, double balance, String password) {
setName(name); // 调用set方法进行验证
setBalance(balance); // 调用set方法进行验证
setPassword(password); // 调用set方法进行验证
}
// 验证并设置name属性
public void setName(String name) {
if (name.length() >= 2 && name.length() <= 4) {
this.name = name;
} else {
System.out.println("名字长度应在2到4个字符之间,设置默认值'Unknown'");
this.name = "Unknown"; // 设置默认值
}
}
// 获取name属性
public String getName() {
return name;
}
// 验证并设置balance属性
public void setBalance(double balance) {
if (balance > 20) {
this.balance = balance;
} else {
System.out.println("余额必须大于20,设置默认余额为20");
this.balance = 20; // 设置默认值
}
}
// 获取balance属性
public double getBalance() {
return balance;
}
// 验证并设置password属性
public void setPassword(String password) {
if (password.length() == 6) {
this.password = password;
} else {
System.out.println("密码必须是6位数字,设置默认密码'123456'");
this.password = "123456"; // 设置默认值
}
}
// 获取password属性
public String getPassword() {
return password;
}
// 展示账号信息
public void showInfo() {
System.out.println("用户名: " + name);
System.out.println("余额: " + balance);
System.out.println("密码: " + password);
}
}
示例输出:
Account account = new Account("Tom", 15, "1234");
account.showInfo();
输出:
名字长度应在2到4个字符之间,设置默认值'Unknown'
余额必须大于20,设置默认余额为20
密码必须是6位数字,设置默认密码'123456'
用户名: Unknown
余额: 20.0
密码: 123456
总结
封装的核心思想是通过隐藏类的内部实现细节并提供必要的访问接口,确保数据的安全性和正确性。使用封装可以增强代码的可维护性和可读性,并且提供了数据验证的机会。
059. Java(继承)
1. 为什么需要继承
继承是一种提高代码复用性和扩展性的重要机制,它允许我们通过创建新的类(子类),基于现有的类(父类或超类)扩展其功能,而无需重复编写相同的代码。继承的两个主要优点是:
- 提高代码的复用性:通过继承,子类可以自动拥有父类的属性和方法,避免重复代码。
- 支持多态:继承是实现多态的前提,通过多态,父类引用可以指向子类对象,增加程序的灵活性和可扩展性。
2. 继承的注意事项
a) 继承的语法
在 Java 中,使用 extends
关键字来声明一个类继承另一个类:
class 子类 extends 父类 {
// 子类的扩展部分
}
- 父类(基类、超类):被继承的类。
- 子类(派生类):继承父类并扩展其功能的类。
b) 当多个类有相同的属性和方法时,可以抽象出父类
如果有多个类具有相同的属性和方法,可以将这些相同的部分抽象到父类中。这样,所有的子类可以通过继承父类来获得这些属性和方法,避免代码重复。
c) 子类自动拥有父类的属性和方法
继承后,子类会自动拥有父类中的非私有(private
)属性和方法,子类可以直接使用这些功能。注意,子类不能访问父类的私有成员,但可以通过父类的公共方法间接访问这些成员。
d) 访问修饰符对继承的影响
访问修饰符(如
public
、protected
、private
)会影响继承关系中子类对父类成员的可见性:
public
:子类可以访问。protected
:子类可以访问。default
(无修饰符):仅限于同包的子类可以访问。private
:子类不能访问,但可以通过公共方法访问。
e) Java 中是单继承机制
Java 中,一个类只能继承一个直接父类。这是为了避免“菱形继承问题”,即多个父类之间存在相同属性或方法时的冲突。不过,Java 可以通过接口来实现类似多继承的效果(一个类可以实现多个接口)。
f) 所有类都是 Object
类的子类
在 Java 中,所有的类都默认继承自 Object
类。Object
类是所有类的父类,它提供了一些基础的方法(如
toString()
和
equals()
),这些方法可以被所有类继承或重写。
g) 多层继承与关系查看
可以通过工具或IDE中的继承结构图来查看类的继承关系,这在多层继承的情况下尤其有用。
总结
继承是 Java 面向对象编程中的重要机制之一,它通过复用代码、减少重复和提供多态性,大大增强了代码的可维护性和扩展性。在使用继承时,特别要注意 Java 的单继承机制、访问控制和父类与子类之间的关系。
060. Java(父类构造器)
父类构造器的调用规则
默认调用父类的无参构造器:
当创建子类对象时,不论子类的哪个构造器被使用,默认情况下,都会调用父类的无参构造器。这是Java中的隐式行为。如果父类没有提供无参构造器,编译会报错。
super(); // 隐式调用
调用带参构造器的情况:
如果父类没有提供无参构造器,或者希望在子类构造器中调用父类的特定构造器,则需要在子类构造器中显式使用
super(参数列表)
来调用父类的带参构造器。- 注意:
super()
或this()
(调用本类的其他构造器)必须放在构造器的第一行,且不能同时出现于同一个构造器中。
- 注意:
super(参数列表); // 显式调用带参数的父类构造器
递归调用至
Object
类:父类构造器的调用并不仅限于直接父类。如果父类还有父类,系统会沿着继承链递归调用,直到最终调用
Object
类的构造器为止。Object
类是所有类的顶级父类。
总的来说,Java的构造器调用顺序遵循“先父后子”的原则。
061. Java(super)
super
关键字的作用与使用
为什么需要
super
:- 当子类和父类具有相同名称的属性和方法时,默认情况下子类会覆盖父类的实现。为了访问父类的属性或方法,必须通过
super
来调用。 super
代表父类对象的引用,主要用于访问父类的属性、方法或构造器。
a) 当子类覆盖了父类的方法或属性时,
super
可以让子类访问被隐藏的父类实现。b)
super
不仅仅用于调用父类的构造器,还可以用于访问父类的字段和方法。- 当子类和父类具有相同名称的属性和方法时,默认情况下子类会覆盖父类的实现。为了访问父类的属性或方法,必须通过
访问父类的属性(受到访问修饰符影响):
- 使用
super
可以直接访问父类的属性,格式如下:
super.属性名;
- 例如,假设父类中有一个名为
name
的属性,如果子类想要访问父类的这个属性,可以通过super.name
。
- 使用
访问父类的方法(受到访问修饰符影响):
- 使用
super
可以调用父类的方法,格式如下:
super.方法名(参数列表);
- 例如,如果父类有一个
display()
方法,子类可以通过super.display()
调用父类版本的display()
方法。
- 使用
访问父类的构造器:
- 在子类构造器中可以通过
super(参数列表)
显式调用父类的构造器,常用于初始化父类的部分。
super(参数列表);
- 这一点在构造子类对象时非常重要,尤其是当父类没有无参构造器时,子类必须使用
super
来调用父类的带参构造器。
- 在子类构造器中可以通过
总结
super
是Java继承机制中的一个重要工具,确保在子类中仍然可以访问父类的被覆盖成员。通过
super
,子类不仅可以调用父类的方法,还可以确保父类的构造器被正确调用,实现合理的继承链初始化。
062. Java(继承练习)
1. B b = new B();
会输出什么?
代码结构如下:
class A {
A() { System.out.println("A类无参"); }
A(String name) { System.out.println("A类有参"); }
}
class B extends A {
B() { this(""); System.out.println("B类无参"); }
B(String name) { System.out.println("B类有参"); }
}
执行 B b = new B();
时,会发生以下事件:
- 首先调用
B
类的无参构造器B() { this(""); System.out.println("B类无参"); }
。 this("")
调用了B
类的有参构造器B(String name)
。- 在
B(String name)
构造器中没有显式调用super()
,因此隐式调用了父类A
的无参构造器A()
。 A()
执行,输出"A类无参"
。- 然后返回到
B(String name)
构造器,执行System.out.println("B类有参");
,输出"B类有参"
。 - 最后返回到
B()
构造器,执行System.out.println("B类无参");
,输出"B类无参"
。
所以,最终的输出顺序为:
A类无参
B类有参
B类无参
2. C c = new C();
会输出什么?
代码结构如下:
class A {
A() { System.out.println("A类"); }
}
class B extends A {
B() { System.out.println("B类无参"); }
B(String name) { System.out.println(name + "B类有参"); }
}
class C extends B {
C() { this("Hello"); System.out.println("C类无参"); }
C(String name) { super("Java"); System.out.println("C类有参"); }
}
执行 C c = new C();
时,会发生以下事件:
- 首先调用
C
类的无参构造器C() { this("Hello"); System.out.println("C类无参"); }
。 this("Hello")
调用了C
类的有参构造器C(String name)
。- 在
C(String name)
构造器中,显式调用了super("Java")
,这会调用B
类的有参构造器B(String name)
。 B(String name)
执行,输出"JavaB类有参"
。- 返回到
C(String name)
构造器,执行System.out.println("C类有参");
,输出"C类有参"
。 - 然后返回到
C()
构造器,执行System.out.println("C类无参");
,输出"C类无参"
。
所以,最终的输出顺序为:
A类
JavaB类有参
C类有参
C类无参
3.代码结构
class A {
String name = "AA";
private int age = 100;
public void test() { }
}
class B extends A {
String java = "java";
private int nums;
public void demo() {
// super可以访问哪些成员(属性和方法)
// this可以访问哪些成员
}
}
class C extends B {
String name = "BB";
public void test() { }
private void show() {
// super可以访问哪些成员(属性和方法)
// this可以访问哪些成员
}
}
分析
1. class B
中
demo()
方法的 super
和 this
super
:在B
类中,super
引用的是直接父类A
的成员。可以通过super
访问A
类中的非私有属性和方法。因此在demo()
中,super
可以访问:name
:因为它是A
类中的非私有属性。test()
:因为它是A
类中的公共方法。
但是,
super
不能访问:age
:因为age
是A
类中的私有属性,私有成员不能被子类直接访问。
总结:
super.name; // 可访问
super.test(); // 可访问
// super.age; // 不可访问,age 是 private
this
:this
引用的是当前对象实例,因此可以访问当前类(B
)以及所有父类中非私有的成员。在demo()
中,this
可以访问:java
:当前类B
的属性。nums
:当前类B
的私有属性(因为this
是访问当前类的)。name
:A
类中的属性,因为它是非私有的。test()
:来自A
类的公共方法。
this.java; // 可访问
this.nums; // 可访问
this.name; // 可访问 (继承自 A 类)
this.test(); // 可访问 (继承自 A 类)
2. class C
中
show()
方法的 super
和 this
super
:在C
类中,super
引用的是直接父类B
的成员。在show()
方法中,super
可以访问:java
:因为它是B
类中的非私有属性。demo()
:因为它是B
类中的公共方法。name
:可以访问B
类中继承自A
类的name
属性。
注意:虽然
C
类也定义了name
属性,但super
引用的是父类B
中的name
属性,而不是C
类中的name
。总结:
super.java; // 可访问
super.demo(); // 可访问
super.name; // 可访问 (继承自 A 类)
this
:在C
类中,this
引用的是当前对象实例,因此它可以访问当前类C
和所有父类中非私有的成员。在show()
方法中,this
可以访问:name
:C
类中的属性(而非A
类中的name
)。test()
:C
类中重写的test()
方法,而不是A
类中的test()
。java
:继承自B
类的非私有属性。demo()
:继承自B
类的公共方法。
this.name; // 可访问 (C 类的 name)
this.test(); // 可访问 (C 类的 test)
this.java; // 可访问 (继承自 B 类)
this.demo(); // 可访问 (继承自 B 类)
总结
super
用于访问父类的非私有成员。在B
类中,它访问A
类的非私有成员;在C
类中,它访问B
类的非私有成员。this
用于访问当前类和父类中的非私有成员。this
可以访问当前类的属性和方法,也可以访问父类中的继承成员。
063. Java(方法重写)
1. 为什么需要重写(Override)?
重写的目的是让子类根据自己的需要,修改从父类继承的方法的实现。通过重写,子类可以提供自己特有的功能或行为,同时保持与父类接口的一致性。这是实现多态(Polymorphism)的关键所在。
2. 子类可以重写父类属性和方法
子类可以通过重写来重新定义父类中的某些方法,使得这些方法在子类中的行为与父类中的实现不同。属性不能直接重写,但可以通过重写其相关的访问方法(如
getter
、setter
)间接实现属性行为的改变。
3. 方法重写的特点
a) 方法重写也叫方法覆盖(Override)
- 子类重写父类的方法时,方法的名称、参数列表必须与父类相同。
b) 子类方法的名称和参数与父类完全相同
- 方法的名称、参数类型、参数顺序、参数数量必须和父类一致。如果不一致,则不是重写,而是重载(Overload)。
c) 子类方法的返回类型
- 子类重写方法时,返回类型可以与父类相同,或者是父类返回类型的子类。
- 例如,父类方法返回类型为
Object
,则子类重写方法的返回类型可以是String
(因为String
是Object
的子类)。
- 例如,父类方法返回类型为
d) 子类方法的访问权限
- 子类重写父类的方法时,不能缩小父类方法的访问权限。例如,父类方法是
public
,子类方法也必须是public
或者是更高的权限,不能将其修改为protected
或private
。
4. 重载(Overload)与重写(Override)的区别
重载(Overload):
- 发生在同一个类中,方法名相同,但参数的类型、数量或顺序不同。
- 与方法的返回值类型和修饰符无关。
- 通常用于在同一个类中提供多个具有不同参数的实现。
public class Example {
// 重载方法
public void method(int x) {
// 实现
}
public void method(String x) {
// 实现
}
}
重写(Override):
- 发生在子类和父类之间,方法名和参数列表必须相同,返回类型要么相同,要么是父类方法返回类型的子类。
- 子类方法的访问权限不能比父类更严格。
class Parent {
public Object getInfo() {
return new Object();
}
}
class Child extends Parent {
@Override
public String getInfo() { // 返回类型可以是父类返回类型的子类
return "Child Info";
}
}
5. 方法重写的例子
假设有如下父类和子类,父类有一个方法
show()
,子类对其进行了重写:
class Parent {
public void show() {
System.out.println("Parent show");
}
}
class Child extends Parent {
@Override
public void show() {
System.out.println("Child show");
}
}
当你调用 Child
类的 show()
方法时,会输出
"Child show"
,因为子类已经重写了父类的实现。
6. 为什么方法重写对面向对象编程很重要?
- 实现多态:重写是实现多态的关键。通过重写,子类可以提供自己对某种行为的具体实现,从而使得父类引用指向子类对象时,调用的是子类的实现,而不是父类的实现。
- 扩展父类功能:在不修改父类的基础上,子类可以通过重写父类的方法来添加或修改功能,从而满足更为具体的需求。
总结
- 方法重写:子类重写父类的方法,方法名称、参数列表相同,返回类型可以是父类返回类型的子类,访问权限不能缩小。
- 方法重载:在同一个类中,方法名称相同,但参数列表不同,与返回类型无关。
064. Java(代码块)
1. 静态代码块
静态代码块是用 static {}
包裹的代码块,属于类而不是类的实例,在类加载时执行。其特点如下:
- 可以定义多个:静态代码块可以有多个,执行顺序根据它们在类中的定义顺序来决定。
- 执行时机:随着类的加载而执行,并且只执行一次。静态代码块的执行早于普通代码块,因为类加载发生在创建对象之前。
- 用途:静态代码块通常用于初始化静态变量或进行一些类级别的初始化操作。
示例:
class Example {
static {
System.out.println("Static block 1");
}
static {
System.out.println("Static block 2");
}
public Example() {
System.out.println("Constructor");
}
}
输出结果:
Static block 1
Static block 2
2. 普通代码块
普通代码块是用 {}
包裹的代码块,属于类的实例。它们的特点如下:
- 可以定义多个:与静态代码块类似,可以定义多个,按照定义顺序执行。
- 执行时机:每次创建对象时执行,且优先于构造器执行,执行顺序高于构造器。在构造器调用之前,普通代码块会按照定义顺序执行,帮助提高代码的复用性,避免在每个构造器中重复初始化代码。
- 用途:通常用于对象级别的初始化操作。
示例:
class Example {
{
System.out.println("Instance block 1");
}
{
System.out.println("Instance block 2");
}
public Example() {
System.out.println("Constructor");
}
}
输出结果(每次实例化对象时):
Instance block 1
Instance block 2
Constructor
3. 类什么时候被加载
Java 中的类是在以下几种情况下被加载到 JVM 中的:
- 创建类对象实例时:当使用
new
关键字创建一个类的实例时,类会被加载到内存中。如果该类继承了父类,那么父类也会被加载。
A a = new A(); // A 类和它的父类将被加载
- 创建子类对象实例时:如果创建的是子类对象,父类会先被加载,之后再加载子类。
- 使用类的静态成员时:如果某个类的静态成员被访问,那么该类会在第一次使用时加载。
System.out.println(ClassName.staticVariable); // ClassName 被加载
- 使用类的静态方法时:与静态变量类似,静态方法的调用也会导致类的加载。
ClassName.staticMethod(); // ClassName 被加载
示例:
class Parent {
static {
System.out.println("Parent static block");
}
{
System.out.println("Parent instance block");
}
public Parent() {
System.out.println("Parent constructor");
}
}
class Child extends Parent {
static {
System.out.println("Child static block");
}
{
System.out.println("Child instance block");
}
public Child() {
System.out.println("Child constructor");
}
}
public class Test {
public static void main(String[] args) {
new Child();
}
}
输出结果:
Parent static block
Child static block
Parent instance block
Parent constructor
Child instance block
Child constructor
4. 创建对象时的调用顺序
在 Java 中,创建对象时的执行顺序遵循以下规则:
- 父类的静态代码块和静态属性:首先执行父类的静态代码块和静态属性的初始化,静态代码块只执行一次。
- 子类的静态代码块和静态属性:接着执行子类的静态代码块和静态属性的初始化,静态代码块也只执行一次。
- 父类的普通代码块和普通属性初始化:然后执行父类的普通代码块和普通属性的初始化,每次实例化都会执行这些代码。
- 父类的构造方法:执行父类的构造方法,完成父类对象的初始化。
- 子类的普通代码块和普通属性初始化:执行子类的普通代码块和普通属性的初始化。
- 子类的构造方法:最后执行子类的构造方法,完成子类对象的初始化。
5. 静态代码块与普通代码块的区别
- 静态代码块只能调用静态成员,即类级别的成员(静态变量和静态方法)。
- 普通代码块可以调用类中的任何成员(包括静态成员和实例成员),因为普通代码块是实例化对象时执行的,可以访问所有对象属性和方法。
6. 执行 new Test();
会输出什么?
根据图片中的代码片段,代码结构如下:
class Demo {
Demo(String s) {
System.out.println(s);
}
Demo() {
System.out.println("Demo无参");
}
}
class Test {
Demo demo1 = new Demo("Demo1初始化");
static Demo demo2 = new Demo("Demo2初始化");
static {
System.out.println("static块执行");
if (demo2 == null) {
System.out.println("demo2 is null");
}
}
Test() {
System.out.println("Test无参");
}
}
当执行 new Test();
时,发生以下执行顺序:
静态成员和静态代码块(在类加载时执行):
- 静态变量
demo2
被初始化,调用new Demo("Demo2初始化")
,输出:
Demo2初始化
- 执行
Test
类的静态代码块,输出:
static块执行
因为 `demo2` 已经初始化,不会输出 `demo2 is null`。
- 静态变量
实例成员和构造方法(在对象实例化时执行):
- 执行实例成员
demo1
的初始化,调用new Demo("Demo1初始化")
,输出:
Demo1初始化
- 执行
Test
类的构造方法,输出:
Test无参
- 执行实例成员
最终输出结果
综合以上分析,当执行 new Test();
时,输出结果是:
Demo2初始化
static块执行
Demo1初始化
Test无参
总结
- 静态成员和静态代码块在类加载时执行,并且只执行一次。
- 普通成员和普通代码块在每次实例化对象时执行。
- 对象的构造方法在普通成员和普通代码块执行后被调用。
065. Java(多态1)
为什么需要多态:
多态是面向对象编程中的核心概念,通过它可以在运行时根据对象的实际类型来调用方法,而不是依赖于编译时的类型。它使得代码更具灵活性和可扩展性。
多态参数:
当参数定义为父类类型时,可以传递任意子类类型的对象,这就是多态的参数应用。这样做可以提高程序的通用性和可维护性。
向上转型:
- a) 语法:父类类型 引用名 = new 子类类型();
- 将子类对象赋值给父类引用。
- b) 父类引用指向子类对象。
- 即使引用类型是父类,但引用的对象实际是子类。
- c) 编译类型看左边,运行类型看右边。
- 编译时检查的是引用的类型(父类),但运行时根据对象的实际类型(子类)决定调用哪个方法。
- d) 可以调用父类中的所有成员(受访问修饰符影响)。
- 使用父类引用只能访问父类中的成员,但这些成员可能在子类中被重写。
- e)
调用的时候,子类有就用子类的成员,子类没有就找父类(就近原则)。
- 如果子类中重写了父类的方法或属性,则优先调用子类中的;否则调用父类中的成员。
- f) 不能调用子类中特有的成员。
- 父类引用不能访问子类中新添加的成员。
- a) 语法:父类类型 引用名 = new 子类类型();
注意事项:
- a) 多态的前提是:两个对象(类)存在继承关系。
- 只有在父类和子类之间存在继承关系时,才能实现多态。
- b) 属性的值看编译类型。
- 成员变量(属性)在编译时看的是父类类型,而不是运行时的子类类型。
- c)
只要编译能通过,方法都是运行看类型,属性看编译类型。
- 方法调用遵循运行时多态机制,而属性的访问则根据编译时的类型决定。
- d)
instanceof
比较操作符用于判断对象的运行类型。instanceof
操作符可以用来判断一个对象是否是某个类或其子类的实例。
- e) 父类引用指向子类对象,想要向下转型(实际就是强转)时,必须确保对象确实是该子类类型,否则会抛出转换错误。
- a) 多态的前提是:两个对象(类)存在继承关系。
向上转型示例
在Java中,多态是通过继承和方法重写来实现的。以下是一个简单的Java多态示例,演示了向上转型和方法重写的使用:
示例代码:
// 父类
class Animal {
// 父类的方法
public void makeSound() {
System.out.println("动物发出声音");
}
}
// 子类1:Dog类
class Dog extends Animal {
// 重写父类的方法
@Override
public void makeSound() {
System.out.println("狗叫:汪汪");
}
}
// 子类2:Cat类
class Cat extends Animal {
// 重写父类的方法
@Override
public void makeSound() {
System.out.println("猫叫:喵喵");
}
}
public class PolymorphismExample {
public static void main(String[] args) {
// 向上转型:父类引用指向子类对象
Animal myAnimal = new Animal(); // 创建父类对象
Animal myDog = new Dog(); // 创建子类Dog对象
Animal myCat = new Cat(); // 创建子类Cat对象
// 调用父类方法,根据运行时的实际对象类型调用相应的重写方法
myAnimal.makeSound(); // 输出:动物发出声音
myDog.makeSound(); // 输出:狗叫:汪汪
myCat.makeSound(); // 输出:猫叫:喵喵
}
}
解释:
- 父类
Animal
定义了一个通用的方法makeSound()
。 - 子类
Dog
和Cat
继承了Animal
,并且重写了makeSound()
方法。 - 在
main
方法中,我们创建了三个对象:myAnimal
,myDog
和myCat
。虽然myDog
和myCat
的类型是Animal
,但是由于实际对象是Dog
和Cat
,所以调用的都是子类中重写的方法。 - 这就是多态的体现:在编译时,编译器只知道引用是
Animal
类型,但在运行时,JVM 会根据实际的对象类型来调用相应的重写方法。
输出结果:
动物发出声音
狗叫:汪汪
猫叫:喵喵
通过这个例子,你可以看到 Java 的多态机制如何根据实际对象类型调用正确的方法。
直接创建类对象示例
直接创建了各自类的对象,并且每个对象的类型与它所引用的类相匹配。下面是代码创建方式的解释:
// 父类
class Animal {
public void makeSound() {
System.out.println("动物发出声音");
}
}
// 子类1:Dog类
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("狗叫:汪汪");
}
}
// 子类2:Cat类
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("猫叫:喵喵");
}
}
public class PolymorphismExample {
public static void main(String[] args) {
// 直接创建各自的类对象
Animal myAnimal = new Animal(); // 创建父类对象
Dog myDog = new Dog(); // 创建Dog对象
Cat myCat = new Cat(); // 创建Cat对象
// 调用对象自己的方法
myAnimal.makeSound(); // 输出:动物发出声音
myDog.makeSound(); // 输出:狗叫:汪汪
myCat.makeSound(); // 输出:猫叫:喵喵
}
}
这种方式和之前的方式的区别:
在你的这种方式下,
myDog
和myCat
都使用了它们的具体类型(Dog
和Cat
),这样可以直接调用Dog
或Cat
类中特有的方法,而不需要向下转型。这适合当你需要访问子类特定的功能时使用。例如,如果
Dog
类中有一个特定的方法bark()
,你可以直接调用:
myDog.bark(); // 假设Dog类中有一个特有的bark()方法
- 与多态性相关的方式是使用 向上转型,即将子类对象赋值给父类引用。这样可以利用多态特性,在运行时动态调用子类的重写方法。即:
Animal myDog = new Dog(); // 父类引用指向子类对象
myDog.makeSound(); // 输出:狗叫:汪汪
使用这种方式时,虽然可以调用重写的方法,但无法调用子类特有的方法(如 `bark()`),除非进行 **向下转型**。
小结:
- 你提供的创建方式是可以的,它直接使用各自的类型引用对应的对象,便于访问子类的特有功能。
- 如果你不需要访问子类的特有方法,而只关心通用的父类方法时,可以使用多态(向上转型)的方式。
向下转型示例
在Java中,向下转型(Downcasting)是指将父类的引用强制转换为子类的引用。这样做的前提是父类引用实际指向的是子类对象,否则会抛出
ClassCastException
。向下转型通常用在需要访问子类特有的方法或属性时。
以下是向下转型的一个示例:
示例代码:
// 父类
class Animal {
public void makeSound() {
System.out.println("动物发出声音");
}
}
// 子类1:Dog类
class Dog extends Animal {
public void makeSound() {
System.out.println("狗叫:汪汪");
}
// Dog类特有的方法
public void bark() {
System.out.println("狗在吠叫");
}
}
// 子类2:Cat类
class Cat extends Animal {
public void makeSound() {
System.out.println("猫叫:喵喵");
}
// Cat类特有的方法
public void purr() {
System.out.println("猫在打呼噜");
}
}
public class DowncastingExample {
public static void main(String[] args) {
// 向上转型:父类引用指向子类对象
Animal myAnimal = new Dog(); // 实际上是Dog对象
// 调用重写的方法
myAnimal.makeSound(); // 输出:狗叫:汪汪
// 向下转型,访问子类特有的方法
if (myAnimal instanceof Dog) {
Dog myDog = (Dog) myAnimal; // 向下转型
myDog.bark(); // 调用Dog类特有的方法
}
// 向上转型:父类引用指向另一个子类对象
Animal anotherAnimal = new Cat(); // 实际上是Cat对象
// 调用重写的方法
anotherAnimal.makeSound(); // 输出:猫叫:喵喵
// 向下转型,访问子类特有的方法
if (anotherAnimal instanceof Cat) {
Cat myCat = (Cat) anotherAnimal; // 向下转型
myCat.purr(); // 调用Cat类特有的方法
}
}
}
解释:
- 向上转型:
Animal myAnimal = new Dog();
父类引用myAnimal
实际上指向的是Dog
对象。这是向上转型,利用多态机制调用Dog
类重写的makeSound()
方法,输出 "狗叫:汪汪"。
- 向下转型:
- 在需要调用
Dog
类中特有的bark()
方法时,必须将myAnimal
转换为Dog
类型。这就是向下转型,代码如下:
- 在需要调用
Dog myDog = (Dog) myAnimal; // 强制类型转换
myDog.bark(); // 调用Dog类特有的方法
- 类型检查:
- 为了避免
ClassCastException
,我们可以使用instanceof
进行类型检查,确保对象的实际类型是我们要转换的类型:
- 为了避免
if (myAnimal instanceof Dog) {
Dog myDog = (Dog) myAnimal;
myDog.bark();
}
- 对
Cat
类的操作:- 类似地,
anotherAnimal
实际上是Cat
对象,通过向下转型后,可以调用Cat
类中特有的purr()
方法:
- 类似地,
if (anotherAnimal instanceof Cat) {
Cat myCat = (Cat) anotherAnimal;
myCat.purr();
}
输出结果:
狗叫:汪汪
狗在吠叫
猫叫:喵喵
猫在打呼噜
注意:
- 向下转型的风险:如果父类引用指向的不是子类对象,而强制进行向下转型,程序会抛出
ClassCastException
。因此,使用向下转型时通常配合instanceof
来进行类型检查,确保安全。 - 实际用途:向下转型一般在处理对象集合或者框架中,特别是当你知道父类引用实际上是某个特定子类的实例时,向下转型允许你调用该子类中特有的方法。
066. Java(多态2)
5. 多态数组
该部分介绍了如何使用多态数组来存储父类和子类对象。具体说明如下:
- 数组定义:数组的类型为父类类型(例如
Person
),但存储的实际元素类型可以是子类类型(如Student
和Teacher
)。 - 操作:创建一个
Person
类,以及Student
和Teacher
的子类对象。将这些对象放入一个Person
类型的数组中,随后遍历该数组并调用特定子类的study
和teach
方法,展示多态的应用。
6. 练习题
代码解析
class A {
int count = 100;
public void display() {
System.out.println(this.count);
}
}
class B extends A {
int count = 200;
public void display() {
System.out.println(this.count);
}
}
B b = new B();
System.out.println(b.count); // 1
b.display(); // 2
A a = b;
System.out.println(a == b); // 3
System.out.println(a.count); // 4
a.display(); // 5
运行结果分析:
System.out.println(b.count);
- 由于
b
是类B
的对象,b.count
将访问类B
中定义的count
变量。 - 输出:
200
- 由于
b.display();
- 这里调用的是
b
对象的display()
方法,由于b
是类B
的实例,执行类B
的display()
方法。 - 该方法中
this.count
引用了类B
中的count
变量,因此输出200
。 - 输出:
200
- 这里调用的是
System.out.println(a == b);
- 这里的
a
被赋值为b
,因此a
和b
指向同一个对象,a == b
将返回true
。 - 输出:
true
- 这里的
System.out.println(a.count);
- 由于
a
是A
类型,但实际引用的是B
对象,此时会访问A
类中的count
变量,而不是B
中的count
,因为成员变量不会发生覆盖(成员变量的访问是根据引用类型,而不是实际对象类型)。 - 输出:
100
- 由于
a.display();
- 尽管
a
是A
类型,但由于它实际指向的是B
的实例,因此调用的是B
类中的display()
方法。这是因为方法是动态绑定的(多态机制)。 - 类
B
的display()
方法中输出的是this.count
,此处this
代表的是B
类的实例,所以输出200
。 - 输出:
200
总结
运行结果依次为:
200
200
true
100
200
这个练习展示了Java中成员变量和方法在继承和多态情况下的行为差异:
- 尽管
- 成员变量:引用类型决定访问的成员变量,即使引用指向的是子类对象,也会访问父类中的变量。
- 方法:方法是动态绑定的,根据实际对象类型来调用方法,即使引用类型是父类,调用的方法仍然是子类的重写方法。
这是一道关于 继承 和 多态 的代码练习题,主要展示了以下几点:
- 父类和子类的字段和方法:类
A
有一个字段count
和一个display
方法,类B
继承自A
,并且重写了count
字段和display
方法。 - 测试代码的多态性:通过实例化子类
B
,并将其赋值给父类A
类型的引用,展示了成员变量和方法在继承和多态下的行为差异。具体代码执行结果已经在上一个回答中详细分析。
总结
- 多态数组:使用父类数组存储子类对象,利用多态特性在运行时调用子类特有的方法。
- 继承与多态:在子类重写父类的字段和方法后,字段访问是基于引用类型,而方法调用是基于对象的实际类型(动态绑定)。