Java入门
Java是由Sun Microsystems公司于1995年5月推出的Java面向对象程序设计语言和Java平台的总称。由詹姆斯·高斯林(James Gosling)和同事们共同研发,并在1995年正式推出。
Java分为三个体系:
JavaSE(J2SE)(Java2 Platform Standard Edition,java平台标准版)
JavaEE(J2EE)(Java 2 Platform,Enterprise Edition,java平台企业版)
JavaME(J2ME)(Java 2 Platform Micro Edition,java平台微型版)
2005年6月,JavaOne大会召开,SUN公司公开Java SE 6。此时,Java的各种版本已经更名以取消其中的数字"2":J2EE更名为Java EE, J2SE更名为Java SE,J2ME更名为Java ME。
2009年,sun公司被oracle收购.
2018年,开源组织Eclipse基金会宣布将JavaEE(Enterprise Edition)被更名为JakartaEE(雅加达)。
JDK、JRE与JVM
JDK
JDK(Java Development Kit),是Java的标准开发工具包,对于普通用户,只需安装JRE即可运行Java程序,而程序开发者则需要按照JDK来编译、调试程序。
JDK提供了编译、运行Java程序所需的各种资源,如,Java编译器、Java运行环境JRE、常用的Java基础库等等,其安装目录为:
bin:存放各种工具命令,如,常用的javac和java。
conf:存放相关配置文件。
include:存放Java和JVM交互用的头文件。
jmods:存放各个模块的文件。
legal:存放各个模块的授权文件。
lib:存放JDK工具命令的实际执行程序
JRE
JRE(Java Runtime Environment),是运行基于Java语言编写的程序的运行环境,用于解释执行Java的字节码文件,即,JRE是Java的运行环境而非开发环境,故而未包含任何开发工具,如,编译器、调试器,其面向的对象时使用Java程序的用户,提供能够运行Java程序的功能。
从JDK11开始,JDK和JRE都是在一起的,安装后默认没有JRE文件夹,若需要这个单独目录,则可在JDK目录下打开cmd,执行:
bin\jlink.exe --module-path jmods --add-modules java.desktop --output jre
执行完后就会生成JRE目录。
JVM
JVM(Java Virtual Machine,Java虚拟机),JRE是被包含在JDK中的,而JVM又包含在JRE中,其负责解释执行字节码文件,是可运行Java字节码文件的虚拟机,所有平台上的JVM向编译器提供相同的接口,而编译器只需要面向虚拟机,生成虚拟机能识别的代码,然后由虚拟机解释执行。
操作系统中虚拟机的定义:虚拟机是一台逻辑计算机,是指用虚拟化技术,通过隐藏特定计算平台的实际物理特性,为用户提供抽象的、统一的、模拟的计算环境(即,架构在操作系统之上,调用操作系统系统调用等功能)。
JVM执行程序的过程:
加载.class文件
所有的Java程序都会先编译为.class字节码文件,这种字节码文件可在虚拟机上运行,而并不与计算机的操作系统进行交互,而是通过JVM与操作系统进行间接交互,由JVM进行解释执行(逐行执行),由于JVM解释执行过程中需要使用类库,故而JRE包含了lib目录用于存放相关工具命令。
运行.class文件
可在命令行中输入Java字节码文件名,此时会启动计算机上安装的JVM,并将字节码文件加载到内存,再来运行其中的字节码命令。
过程实操:
使用记事本编写Java程序文件(文件名应与类名相同,即为,Hello.java)
public class Hello{
public static void main(String[] args){
System.out.println("Hello World");
}
}
之后将.txt后缀名改为.java(可能需要在文件资源管理器中设置改变后缀名即可改变文件类型的选项)
在cmd中使用javac命令进行编译,得到.class字节码文件:
C:\Users\lenovo>cd Desktop
C:\Users\lenovo\Desktop>javac Hello.java
桌面即可得到:
使用java命令启动虚拟机执行该字节码文件(无后缀名):
C:\Users\lenovo\Desktop>java Hello
Hello World
若我们直接打开字节码文件会出现乱码,可使用以下命令来打开:
C:\Users\lenovo\Desktop>javap -v 字节码文件名
javap是jdk自带的反解析工具。它的作用就是根据class字节码文件,反解析出当前类对应的code区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。而-v选项可打印出输出附加信息,包括行号、本地变量表,反汇编等。
注意:
源文件名:必须和类名相同,当保存文件时,应当使用类名作为文件名保存,文件后置为.java,若文件名与类名不相同则会导致编译错误。
主方法入口:所有的Java程序都由public static void main(String[] args)方法开始执行。
此处使用的java版本:
C:\Users\lenovo>java -version
openjdk version "18.0.2" 2022-07-19
OpenJDK Runtime Environment (build 18.0.2+9-61)
OpenJDK 64-Bit Server VM (build 18.0.2+9-61, mixed mode, sharing)
Java的跨平台特性
高级语言的编译运行方式
编译型语言,如,C、C++等
由相应的编译器来运行,先编写.c代码,然后编译、链接成.obj文件(操作系统中的目标文件),但是针对不同的操作系统都需要重新进行编译,且,若硬件区别较大,代码可能会不能运行,故而,并不是跨平台的语言。
解释型语言,如,Python、Shell等
由相应的解释器来运行,读一行代码,就翻译为一行机器语言去运行,其过程中并不会产生编译文件,故而,Python是开源的,必须将代码交给别人才能进行运行(而无法通过传递目标文件的方式)。
混合型语言(半解释、半编译),如,Java
先整体编译为.class文件,再逐行进行翻译为机器语言,且,其并不直接运行在系统中,而是运行在虚拟机当中,此时,只要针对不同的OS给出不同的虚拟机即可,从而实现了跨平台特性。
跨平台的原理
Java基础语法
注释
单行注释
//注释信息
多行注释
/*注释信息*/
文档注释(可用于生成专用文档,IDEA->设置->文件和代码模板->include)
/**
* @author lenovo
* @version 2023
* @date 2023/1/6 11:24
* @package org.example.polo
*/
关键字
Java关键字即为Java的保留字,这些保留字不能作为常量、变量和任何标识符的名称。
关键字 | 描述 |
abstract | 抽象方法,抽象类的修饰符 |
assert | 断言条件是否满足 |
boolean | 布尔数据类型 |
break | 跳出循环或者label代码段 |
byte | 8-bit 有符号数据类型 |
case | switch语句的一个条件 |
catch | 和try搭配捕捉异常信息 |
char | 16-bit Unicode字符数据类型 |
class | 定义类 |
const | 未使用 |
continue | 不执行循环体剩余部分 |
default | switch语句中的默认分支 |
do | 循环语句,循环体至少会执行一次 |
double | 64-bit双精度浮点数 |
else | if条件不成立时执行的分支 |
enum | 枚举类型 |
extends | 表示一个类是另一个类的子类 |
final | 表示一个值在初始化之后就不能再改变了 表示方法不能被重写,或者一个类不能有子类 |
finally | 为了完成执行的代码而设计的,主要是为了程序的健壮性和完整性,无论有没有异常发生都执行代码。 |
float | 32-bit单精度浮点数 |
for | for循环语句 |
goto | 未使用 |
if | 条件语句 |
implements | 表示一个类实现了接口 |
import | 导入类 |
instanceof | 测试一个对象是否是某个类的实例 |
int | 32位整型数 |
interface | 接口,一种抽象的类型,仅有方法和常量的定义 |
long | 64位整型数 |
native | 表示方法用非java代码实现 |
new | 分配新的类实例 |
package | 一系列相关类组成一个包 |
private | 表示私有字段,或者方法等,只能从类内部访问 |
protected | 表示字段只能通过类或者其子类访问 子类或者在同一个包内的其他类 |
public | 表示共有属性或者方法 |
return | 方法返回值 |
short | 16位数字 |
static | 表示在类级别定义,所有实例共享的 |
strictfp | 浮点数比较使用严格的规则 |
super | 表示基类 |
switch | 选择语句 |
synchronized | 表示同一时间只能由一个线程访问的代码块 |
this | 表示调用当前实例 或者调用另一个构造函数 |
throw | 抛出异常 |
throws | 定义方法可能抛出的异常 |
transient | 修饰不要序列化的字段 |
try | 表示代码块要做异常处理或者和finally配合表示是否抛出异常都执行finally中的代码 |
void | 标记方法不返回任何值 |
volatile | 标记字段可能会被多个线程同时访问,而不做同步 |
while | while循环 |
字面量与变量
字面量
字面量,亦可称为常量,是单纯的数据。
变量
在程序执行过程中,若值有可能发生改变的量,此时可使用变量进行存储,当数据发生变化时,变量名并不会以改变,只需改变变量存储的数据即可。
定义方式:
数据类型 变量名=数据值;
数据类型
Java中的数据类型分为基本数据类型和引用数据类型两种。
基本数据类型
基本数据类型在被创建时,JVM会在栈上给其划分一块内存,将数值直接存储在栈上,且,基本数据类型的变量在作为参数进行传递时,传递的是变量所对应的值(真实数据)。
Java一共有八种基本数据类型:
注意,字符串String是引用数据类型。
引用数据类型
除基本数据类型以外的就都属于引用数据类型,常见的有,String、自定义类、接口、数组、注解等。
与基本数据类型不同,引用数据类型存储的并非是数据本身,而是该数据的引用(相当于C语言中的地址,但Java中并无指针这一概念)在被创建时,首先要在栈上给其引用分配一块内存,而对象所存储的具体信息都存储在堆内存上,然后由栈上面的引用指向堆中对象的地址。且,在数据作为参数传递时,引用数据类型传递的虽是变量存储的值,但该值是指向真实值在堆中地址的引用。
注:Java中并无地址传递而只有值传递,引用数据类型传递的是引用,但本质仍是值传递。
运算符、运算优先级与类型转换
运算符:对字面量或者变量进行操作的符号。
表达式:用运算符把字面量或者变量连接起来,符合Java语法的式子就可以称为表达式。不同运算符连接的表达式体现的是不同类型的表达式。
算术运算符
public class Demo {
@Test
public void test(){
System.out.println("3/2的结果为:"+(3/2));
}
}
此处,3与2均为整型量,而同一数据类型之间的运算并不涉及类型转换,故而运算结果亦取整后为整型。
基本数据类型的转换
注:对于引用数据类型,在"面向对象-多态-多态中成员的调用原则"中有。
取值范围从小到大排序:byte<short<int<long<float<double,这里,以加法操作为例。
隐式转换(自动类型提升)
原则:
取值范围小的和取值范围大的进行运算,小的会先提升为大的,再进行运算。
byte、short、char类型的数据在和取值范围大的数据运算时会直接提升为int再进行运算,其中,char类型会转换为相应的ASCII值。
public class Demo {
@Test
public void test(){
char a=10;
char b=20;
short c=30;
double d=3.14;
if(judgeIsInt(a+b)) System.out.println("(a+b)的运算结果数据类型为:int");
if(judgeIsInt(a+c)) System.out.println("(a+c)的运算结果数据类型为:int");
if(judgeIsDouble(a+d)) System.out.println("(a+c)的运算结果数据类型为:double");
}
public boolean judgeIsInt(Object t){
if(t instanceof Integer)return true;
else return false;
}
public boolean judgeIsDouble(Object t){
if(t instanceof Double)return true;
else return false;
}
}
强制类型转换
若要将一个取值范围大的数值赋值给取值范围小的变量,是不允许直接赋值的,如果一定要这么做就需要加入强制转换。
格式:
目标数据类型 变量名=(目标数据类型)被强转的数据;
注:强制转换是可能出现数据异常的。
public class Demo {
@Test
public void test(){
double a=3.14;
int b=(int) a;
System.out.println("将a强制转为int类型:"+b);
}
}
自增自减运算符
赋值运算符
关系运算符
逻辑运算符
短路逻辑运算符
注:
对于逻辑运算符,无论右边结果是什么都会执行。
对于短路逻辑运算符,如&&中,若左边为false,右边无论为真或假整个表达式一定为false,故右边不执行可提高效率,而||同理。
三元运算符
格式:
关系表达式?表达式1:表达式2;
例:
c=a>b?a:b;
运算优先级
流程控制
跳转语句
break
作用:跳出单层循环。
continue
作用:结束一次循环,继续下一次循环。
return
作用: 直接结束整个方法,从而结束循环。
分支结构
条件语句可根据不同条件执行不同的语句,包括if条件语句和switch多分支语句。
if分支结构
语法格式:
if(expression 1){
//expression1为true时执行
执行语句;
}else if(expression 2){
//expression2为true时执行
执行语句;
}else{
//expression1与expression2都为false时执行
执行语句;
}
注意:
一旦满足某个条件表达式,则进入其执行语句块执行,执行完毕后不会执行其一下的条件语句。
如果多个条件表达式之间为“互斥”关系,多个语句之间可以上下调换顺序,一旦是包含关系,要求条件表达式范围小的写到范围大的上边。
switch分支结构
语法格式
switch (变量){
case 常量1:
执行语句;
case 常量2:
执行语句;
case 常量3:
执行语句;
default:
执行语句;
}
例:
public class Demo {
@Test
public void test(){
Random random=new Random();
int num= random.nextInt(4);
switch (num){
case 0:
System.out.println("Good Morning!");
break;
case 1:
System.out.println("Good Afternoon!");
break;
case 2:
System.out.println("Good Evening!");
break;
default:
System.out.println("Hello World!");
}
}
}
注:
swich(表达式)中表达式的返回值必须是以下几种类型之一:byte,short,char,int,枚举(jdk1.5),String(jdk1.7)
case子句中的值必须是常量,且所有case子句中的值应是不同的;
default子句是可任选的,当没有匹配的case时,执行default;
break语句用来在执行完一个case分支后使程序跳出swich语句块;如果没有break程序会顺序执行到swich结尾;
循环结构
while循环结构
语法格式:
while(判断条件语句) {
循环体语句;
控制条件语句;
}
for循环结构
语法格式:
for(初始化语句;判断条件语句;控制条件语句) {
循环体语句;
}
forEach循环结构
语法格式:
for (声明语句 : 表达式) {
//代码句子
}
例:
public class Test {
public static void main(String args[]) {
int [] numbers = {10, 20, 30, 40, 50};
for ( int x : numbers ) {
System.out.print( x );
System.out.print(",");
}
System.out.print();
String [] names = {"James", "Larry", "Tom", "Lacy"};
for ( String name : names ) {
System.out.print( name );
System.out.print(",");
}
}
}
forEach语句时for语句的简化版,已被称为"增强for语句"。
do...while循环结构
语法格式
do {
循环体语句;
控制条件语句;
} while((判断条件语句);
与while循环结构区别在于,do...while循环语句会先执行语句再进行判断,即,至少会执行一次循环体。
数组
数组指的是一种容器,可用来存储同种数据的多个值,且,数组容器在存储数据时,需要结合隐式转换考虑,例:
int类型数组可存储byte、short、int类型,但不能存储boolean、double类型数据。
double类型数组可存储byte、short、int、long、float、double类型,但是不能存储boolean类型。
数组的基本操作
数组类型变量的定义方式
数据类型 []array;
数据类型 array[];
数组对象的初始化
int []array=new int[n];
//动态初始化,表示只能存储n个相应的元素,事实上,此处做了两件事:
//1.使用int[n]创建了一个数组
//2.将新创建数组的引用赋值给变量array
int []array={1,2,3,4,5};
//静态初始化
注:数组一旦初始化完,其长度就不再改变。
数组对象的引用
public class Demo {
@Test
public void test(){
int []array=new int[]{1,2,3,4,5};
System.out.println(array);
}
}
有关引用值的含义:
[:表示当前对象是数组类型。
I:表示当前数组中的元素都是int类型。
@:一个间隔符,是固定格式。
45c8e616:该对象真正的地址(十六进制)。
事实上,在未重写toString()方法的类中,输出格式是很类似的,如,以下定义一个Person类并输出:
public class Demo {
@Test
public void test(){
Person person=new Person();
System.out.println(person);
}
}
org.example.polo.Person:表示当前Person类的位置。
@:间隔符。
7cf10a6f:该对象真正的地址(十六进制)。
注意:数组属于引用数据类型,故而在使用前一定要开辟空间(实例化),若使用了未开辟空间的数组,则一定会出现NullPointException异常信息。
数组的访问
索引:每一个存储到数组的元素,都会自动的拥有一个编号,从0开始,这个自动编号称为数组索引 (index),可以通过数组的索引访问到数组中的元素。
语法格式:
数组名[索引];
长度属性:每个数组都具有长度,而且是固定的,Java中赋予了数组的一个属性,可以获取到数组的长度。
数组对象.length;
数组的遍历
public class Demo {
@Test
public void test(){
int []array=new int[]{1,2,3,4,5};
for (int i:array) {
System.out.println(i);
}
}
}
二维数组
二维数组的初始化
int[][] array1 = new int[10][10];
int array2[][] = new int[10][10];
int array3[][] = { { 1, 1, 1 }, { 2, 2, 2 } };
int array4[][] = new int[][] { { 1, 1, 1 }, { 2, 2, 2 } };
不定长的二维数组:
int[][] array1 = new int[10][10];
int array2[][] = new int[10][10];
int array3[][] = { { 1, 1, 1 }, { 2, 2, 2 } };
int array4[][] = new int[][] { { 1, 1, 1 }, { 2, 2, 2 } };
获取二维数组长度
int length1 = array.length;
int length2 = array[0].length;
// 获取二维数组的第一维长度(3)
System.out.println(length1);
// 获取二维数组的第一维的第一个数组长度(1)
System.out.println(length2);
数组原理图与程序执行流程分析
注:此处转载了文章https://blog.csdn.net/nishiwodebocai21/article/details/96484891
内存概述
内存是计算机中的重要原件,临时存储区域,作用是运行程序。我们编写的程序是存放在硬盘中的,在硬盘中的程序是不会运行的,必须放进内存中才能运行,运行完毕后会清空内存。 Java虚拟机要运行程序,必须要对内存进行空间的分配和管理。
JVM对内存的划分
JVM将部分内存空间进行了划分,使得每一片区域有特定处理数据方式和内存管理方式。
区域名称 | 作用 |
寄存器 | 给CPU使用,和我们开发无关。 |
本地方法栈 | JVM在使用操作系统功能的时候使用,和我们开发无关。 |
方法区 | 存储可以运行的class文件,包含方法的相关信息。 |
堆内存 | 存储对象或者数组,new来创建的,都存储在堆内存,且,堆内存中的数据都有一个地址值,用16进制表示,亦有默认值,整型为0,浮点型为0.0,字符为'\u0000\',布尔为false,引用类型为null。 |
方法栈 | 存放方法中的局部变量,方法的运行一定要在栈中,且,一旦局部变量超出作用域,就会立刻从栈内存中消失。 |
数组在内存中的存储
public static void main(String[] args) {
int[] arr = new int[3];
System.out.println(arr); // [I@5f150435
}
以上方法执行,输出的结果是[I@5f150435,这个是数组在内存中的地址。new出来的内容,都是在堆内存中存储的,而方法中的变量arr保存的是数组的地址。输出arr[0],就会输出arr保存的内存地址中数组中索引为0上的元素。
main方法进入栈内存中进行执行,由于第一条语句使用了new申请内存空间,故而,在堆内存中申请了两块内存空间来存储数组中的值(刚开始存储默认初始值)。因为堆内存中申请的空间都是带有地址值的,故而,此处将地址值赋值给arr,arr也可通过地址值找到堆内存中的元素,即,栈内存中的arr存储的是地址值而非真实值,而索引通过该地址值在堆内存中进行数据的访问。
程序执行流程:
main方法进入方法栈执行
创建数组,JVM会在堆内存中开辟空间,存储数组
数组在内存中会有自己的内存地址,以十六进制数表示
数组中有2个元素,默认值为0
JVM将数组的内存地址赋值给引用类型变量array
变量array保存的是数组内存中的地址,而不是一个具体数值,因此称为引用数据类型。
两个变量指向一个数组时:
数组的使用
数组作为方法参数
数组作为方法参数传递时,传递的参数是数组在内存中的地址。
public static void main(String[] args) {
int[] arr = { 1, 3, 5, 7, 9 };
//调用方法,传递数组
printArray(arr);
}
/* 创建方法,方法接收数组类型的参数 进行数组的遍历 */
public static void printArray(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
这是由于这样的传递方式,使得若在调用函数内部对数组进行修改,则会影响到原函数中的对象自身。
数组作为方法返回值
数组作为方法的返回值,返回的是数组的内存地址。
public static void main(String[] args) {
//调用方法,接收数组的返回值
//接收到的是数组的内存地址
int[] arr = getArray();
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}
/* 创建方法,返回值是数组类型
创建方法,返回值是数组类型 return返回数组的地址 */
public static int[] getArray() {
int[] arr = { 1, 3, 5, 7, 9 };
//返回数组的地址,返回到调用者
return arr;
}
方法
Java方法是语句的结合,被包含于类或对象中,则程序中被创建,在其他地方被引用。
方法的定义
修饰符 返回值类型 方法名 (参数类型 参数名){
...
方法体
...
return 返回值;
}
组成成分:
修饰符:修饰符,这是可选的,告诉编译器如何调用该方法。定义了该方法的访问类型。
返回值类型 :方法可能会返回值。returnValueType是方法返回值的数据类型。有些方法执行所需的操作,但没有返回值。在这种情况下,returnValueType是关键字void。
方法名:是方法的实际名称。方法名和参数表共同构成方法签名。
参数类型:参数像是一个占位符。当方法被调用时,传递值给参数。这个值被称为实参或变量。参数列表是指方法的参数类型、顺序和参数的个数。参数是可选的,方法可以不包含任何参数。
方法体:方法体包含具体的语句,定义该方法的功能。
权限修饰符
修饰符 | 当前类 | 相同包 | 子类 | 其他包 |
public | √ | √ | √ | √ |
protetcted | √ | √ | √ | × |
default | √ | √ | × | × |
private | √ | × | × | × |
这些访问修饰符既可以修饰类中的属性,又可以修饰类中的方法,而public和default还可以修饰类。在同一个java文件里,公有类有且仅有一个,并且该文件命名为类名.java。
方法调用
Java支持两种调用方法的方式,当程序调用一个方法时,会将程序控制权交给被调用的方法,而当被调用的方法的返回语句被执行或到达方法体闭括号时,会将控制权交还给程序。
public class TestMax {
/** 主方法 */
public static void main(String[] args) {
int i = 5;
int j = 2;
int k = max(i, j);
System.out.println("The maximum between " + i +
" and " + j + " is " + k);
}
/** 返回两个整数变量较大的值 */
public static int max(int num1, int num2) {
int result;
if (num1 > num2){
result = num1;
}else{
result = num2;
}
return result;
}
}
这个程序包含main方法和max方法。Main方法是被JVM调用的,除此之外,main方法和其它方法没什么区别。
方法的参数
方法是程序中最小的执行单元,参数在传递时可分为两种:
形参:形式参数,是指方法定义中的参数。
实参:实际参数,是指方法调用时传递的参数。
Java中,方法参数的传递是值传递。
public class TestPassByValue {
public static void main(String[] args) {
int num1 = 1;
int num2 = 2;
System.out.println("Before swap method, num1 is " +
num1 + " and num2 is " + num2);
// 调用swap方法
swap(num1, num2);
System.out.println("After swap method, num1 is " +
num1 + " and num2 is " + num2);
}
/** 交换两个变量的方法 */
public static void swap(int n1, int n2) {
System.out.println("\tInside the swap method");
System.out.println("\t\tBefore swapping n1 is " + n1
+ " n2 is " + n2);
// 交换 n1 与 n2的值
int temp = n1;
n1 = n2;
n2 = temp;
System.out.println("\t\tAfter swapping n1 is " + n1
+ " n2 is " + n2);
}
}
运行结果:
Before swap method, num1 is 1 and num2 is 2
Inside the swap method
Before swapping n1 is 1 n2 is 2
After swapping n1 is 2 n2 is 1
After swap method, num1 is 1 and num2 is 2
可见,swap方法调用后,实参的值并未改变。
可变参数
Java还支持可变数量的参数,且,每个方法最多只能有一个这样参数个数可变的形参,其必须放在形参列表的最后。事实上,在参数个数可变的形参进行传递时,编译器会把最后一个可变参数的形参转换为数组形参,故而相当于是获取了一个数组形参,此时就需要使用遍历的方式来获取里面的每一个实参。
语法格式:
权限修饰符 返回值类型 方法名(数据类型 ...形参名){
方法体;
}
例:
public class Demo {
@Test
public void test(){
show("Hello World","Good Morning","Good Evening","Good Afternoon");
}
public void show(String ...Str){
for (String str:Str) {
System.out.println(str);
}
}
}
命令行参数的使用
有时候你希望运行一个程序时候再传递给它消息。这要靠传递命令行参数给main()函数实现,命令行参数是在执行程序时候紧跟在程序名字后面的信息。
public class CommandLine {
public static void main(String args[]){
for(int i=0; i<args.length; i++){
System.out.println("args [" + i + "]: " + args[i]);
}
}
}
在命令行窗口中输入命令:
java CommandLine this is a command line 200 -100
运行结果:
args[0]: this
args[1]: is
args[2]: a
args[3]: command
args[4]: line
args[5]: 200
args[6]: -100
方法重载
方法重载是指在一个类中定义多个同名的方法,但要求每个方法具有不同的参数的类型或参数的个数。
注意:
方法名必须相同,参数列表必须不同(个数不同、或类型不同、或参数排列顺序不同)
public int getSum(int a,int b){
return a+b;
}
public double getSum(int a,double b){
return a+b;
}
public int getSum(int a,int b,int c){
return a+b+c;
}
只有返回值或权限修饰符不同,不构成重载。
public int getSum(int a,int b){
return a+b;
}
public double getSum(int a,int b){
//报错:'getSum(int, int)' 与 'getSum(int, int)' 冲突;两个方法具有相同的擦除
return a+b;
}
可见,函数重载的判断依据是方法名和参数列表,而与返回值类型、权限修饰符无关。
对于可变参数,只需将其看做一个数组即可:
public int getSum(int a,int b){
return a+b;
}
public int getSum(int a,int b,int ...c){
int sum=0;
for (int i:c) {
sum+=i;
}
return a+b+sum;
}
但若是:
public int getSum(int a,int b,int ...c){
int sum=0;
for (int i:c) {
sum+=i;
}
return a+b+sum;
}
public int getSum(int a,int b,int[] c){
int sum=0;
for (int i:c) {
sum+=i;
}
return a+b+sum;
}
则会报错,因为二者本质是同一个函数。
底层分析
编写程序:
public class Demo{
public int getSum(int a,int b){
return a+b;
}
public int getSum(int a,int b,int c){
return a+b;
}
}
在命令行窗口进行操作:
C:\Users\lenovo>cd desktop
C:\Users\lenovo\Desktop>javac Demo.java
C:\Users\lenovo\Desktop>javap -v Demo
Classfile /C:/Users/lenovo/Desktop/Demo.class
Last modified 2023年1月6日; size 292 bytes
SHA-256 checksum 1275cd151c2cfc7ceb3dc52a363b9aadf1706a31d5e355fc092972f5f810b476
Compiled from "Demo.java"
public class Demo
minor version: 0
major version: 62
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #7 // Demo
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 3, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Class #8 // Demo
#8 = Utf8 Demo
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 getSum
#12 = Utf8 (II)I
#13 = Utf8 (III)I
#14 = Utf8 SourceFile
#15 = Utf8 Demo.java
{
public Demo();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public int getSum(int, int);
descriptor: (II)I
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: iload_1
1: iload_2
2: iadd
3: ireturn
LineNumberTable:
line 4: 0
public int getSum(int, int, int);
descriptor: (III)I
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=4, args_size=4
0: iload_1
1: iload_2
2: iadd
3: ireturn
LineNumberTable:
line 8: 0
}
SourceFile: "Demo.java"
变量作用域
变量的范围是程序中该变量可以被引用的部分,方法内定义的变量被称为局部变量。局部变量的作用范围从声明开始,直到包含它的块结束。方法的参数范围涵盖整个方法,参数实际上就是一个局部变量。for循环的初始化部分声明的变量,其作用范围在整个循环。但循环体内声明的变量其适用范围是从它声明到循环体结束。它包含如下所示的变量声明:
你可以在一个方法里,不同的非嵌套块中多次声明一个具有相同的名称局部变量,但你不能在嵌套块内两次声明局部变量。
finalize方法
finalize方法,它会在对象被垃圾收集器析构(回收)之前调用,用来清除回收对象。
一般格式:
protected void finalize()
{
// 在这里终结代码
}
关键字protected是一个限定符,它确保finalize() 方法不会被该类以外的代码调用。Java的内存回收可以由JVM来自动完成。如果想要手动使用,则可以使用上面的方法。
public class FinalizationDemo {
public static void main(String[] args) {
Cake c1 = new Cake(1);
Cake c2 = new Cake(2);
Cake c3 = new Cake(3);
c2 = c3 = null;
System.gc(); //调用Java垃圾收集器
}
}
class Cake extends Object {
private int id;
public Cake(int id) {
this.id = id;
System.out.println("Cake Object " + id + "is created");
}
protected void finalize() throws java.lang.Throwable {
super.finalize();
System.out.println("Cake Object " + id + "is disposed");
}
}
运行结果:
Cake Object 1is created
Cake Object 2is created
Cake Object 3is created
Cake Object 3is disposed
Cake Object 2is disposed
面向对象
在理解Java的类和对象之前,先简单介绍一下面向对象的程序设计。程序设计是通过对象对程序进行设计,对象代表一个实体,实体可以清楚地被识别。
Java作为一种面向对象语言。支持以下基本概念:
类
对象
封装
继承
多态
抽象
消息解析
类与对象
类:对象共同特征的描述,是一种提取出来的抽象概念。
对象:类的实例化,是真实存在的具体东西。
类的定义
在Java中,必须先设计类,才能获取其对象。
权限修饰符 class 类名{
1.成员变量(属性)
2.成员方法
3.构造器
4.代码块
5.内部类
}
一个类可以包含以下类型变量:
局部变量:在方法、构造方法或者语句块中定义的变量被称为局部变量。变量声明和初始化都是在方法中,方法结束后,变量就会自动销毁。
成员变量:成员变量是定义在类中,方法体之外的变量。这种变量在创建对象的时候实例化。成员变量可以被类中方法、构造方法和特定类的语句块访问。
类变量:类变量也声明在类中,方法体之外,但必须声明为 static 类型。
注意:
用来描述一类事物的类,专业术语叫做:Javabean类,在Javabean类中,是不写main方法的。
在以前,编写main方法的类,叫做测试类,可以在测试类中创建JavaBean类的对象并进行赋值调用。
一个Java文件中可以定义多个class类,且,只能有一个类是public修饰,其类名必须成为代码文件名,注意,实际开发中,一个文件只定义一个class类(可以内含内嵌类)。
成员变量的完整定义格式是:
修饰符 数据类型 变量名称=初始化值;
事实上,若不指定初始化值,JVM会自动赋予默认值:
内部类
类的五大成员:属性、方法、构造方法、代码块、内部类。
内部类:在一个类中再定义一个类。
例:写一个Javabean类描述汽车
public class Car{
String carName;
int carAge;
int carColor;
String engineName;
int engineAge;
}
//事实上,可以写为:
public class Car{
String carName;
int carAge;
int carColor;
class Engine{//内部类
String engineName;
int engineAge;
}
}
内部类表示的事物是外部类的一部分,且,内部类单独出现并没有任何意义。
内部类的访问特点:
内部类可以直接访问外部类的成员,包括私有。
外部类要访问内部类的成员则必须创建对象。
其中,m表示方法(method),f表示成员变量(field),c则表示内部类(class)。
内部类的分类:成员内部类、静态内部类、局部内部类、匿名内部类。
成员内部类:写在成员位置,属于外部类的成员,例:
public class Car{//外部类
String carName;
int carAge;
int carColor;
class Engine{//成员内部类
String engineName;
int engineAge;
}
}
成员内部类可以备一些修饰符修饰,如,private、默认、protected、public、static等,其规则与修饰普通成员时是一样的。
在JDK16之前,成员内部类内部并不能定义静态变量,JDK16之后才开始可以定义静态变量。
获取成员内部类对象:
在外部类中编译方法,对外提供内部类的对象。
直接创建格式:外部类名.内部类名 对象名=外部类对象.内部类对象;
成员内部类获取外部类的变量:
实现以下输出:
public class Outer{
private int a=10;
class Inner{
private int a=20;
public void show(){
int a=30;
System.out.println(a);//30
System.out.println(this.a);//20
System.out.println(Outer.this.a);//10
}
}
}
静态内部类:只能访问外部类中的静态变量和静态方法,如果想要访问非静态的成员,则需要创建外部类对象。
public class Car{//外部类
String carName;
int carAge;
int carColor;
class Engine{//静态内部类
String engineName;
int engineAge;
}
}
创建静态内部类对象的格式:外部类名.内部类名 对象名=new 外部类名.内部类();
调用静态内部类的非静态方法格式:先创建对象,再使用对象来调用。
调用静态内部诶的静态方法的格式:外部类名.内部类名.方法名();
局部内部类:将内部类直接定义在方法里面的就叫做局部内部类,类似于方法里面的局部变量。外界是无法直接使用,需要在方法内部创建对象并使用。该类可以直接访问外部类的成员,也可以访问方法内的局部变量。
匿名内部类:本质上就是隐藏了名字的内部类。
格式:
new 类名或者接口名(){
重写方法;
};
封装
面向对象的三大特性:封装、继承、多态。
封装:是指,一种将抽象性函式接口的实作细节部份包装、隐藏起来的方法。用于告诉我们,如何正确设计对象的属性和方法,对象代表什么,就得封装对应的数据,并提供数据对应的行为。
封装为类之后,要访问内部的代码和数据就必须通过严格的接口控制。
封装的好处:
对象代表什么,就得封装对应的数据,并提供数据对应的行为。
降低学习成本,可以少学、少记,不用记对象有哪些
构造方法
构造方法也叫做构造器、构造函数,是对对象的初始化语句进行了封装。作用:创建对象,并给对象赋值。
每个类都有构造方法。如果没有显式地为类定义构造方法,Java编译器将会为该类提供一个默认构造方法。在创建一个对象的时候,至少要调用一个构造方法。构造方法的名称必须与类同名,一个类可以有多个构造方法。
构造方法的格式:
public class 类名{
修饰符 类名(参数){
方法体;
}
}
特点:
方法名与类名相同,大小写也一致。
没有返回值类型,亦无void(写上void就代表是普通方法了)。
不能由return语句返回值。
例:
public class Student{
private String name;
private int age;
...
public Student(){
...
}
//空参构造方法
public Student(String name,int age,...){
...
}
//带参数的构造方法
}
执行时机:
创建对象的时候由虚拟机调用,不能手动调用构造方法。
每创建一次对象,就会调用一次构造方法,而对象是在构造方法执行的过程中完成初始化并被赋值。
构造方法的调用:
new 构造方法名(实际参数列表);
注意:
①构造方法的定义
如果没有定义构造方法,系统将默认给出一个默认的无参数构造方法(这个由系统提供的无参数的构造方法也叫做缺省构造器)。
如果定义了构造方法,系统将不再提供默认的构造方法。
②构造方法的重载
Java中的构造方法可分为带参构造方法和无参构造方法,两者方法名相同,但是参数不同,这叫做构造方法的重载。
标准JavaBean
也可称之为实体类,其对象可以用于在查询中封装数据。
规则:
成员变量使用private进行修饰。
提供至少两个构造方法
无参构造方法
带全部参数的构造方法
成员方法
提供每一个成员对象的setXxx()/getXxx()
事实上,在IDEA中创建一个类并设置其成员变量后,使用Alt+Insert可快速进行构造。
例:
构造一个JavaBean:
package org.example.polo;
/**
* @author 悟兰因
* @version 2023
* @date 2023/1/7 10:06
* @package org.example.polo
*/
public class Animal {
private String AName;
private int AAge;
private String AType;
public Animal() {
}
public Animal(String AName, int AAge, String AType) {
this.AName = AName;
this.AAge = AAge;
this.AType = AType;
}
public String getAName() {
return AName;
}
public void setAName(String AName) {
this.AName = AName;
}
public int getAAge() {
return AAge;
}
public void setAAge(int AAge) {
this.AAge = AAge;
}
public String getAType() {
return AType;
}
public void setAType(String AType) {
this.AType = AType;
}
}
访问实例变量和调用成员方法:
public class Demo {
@Test
public void test1(){
//1.创建对象
Animal animal=new Animal("Mike",3,"Dog");
//2.设置animal的名称
animal.setAName("Tom");
System.out.println("I'm a "+animal.getAType()+",My name is "+animal.getAName()+",I'm "+animal.getAAge()+" now!");
}
}
this关键字及其内存原理
this关键字代表当前所在函数所属对象的引用,即,哪个对象调用了this所在的函数,this就代表哪个对象。
注意:一般将需要先创建类的对象才能进行调用的方法与属性称之为实例方法、实例属性,相应的对象也可称为实例类,相对应的,不需要创建类的对象就可调用的方法和属性称之为静态方法、静态属性,在this的定义中可知,其存在前提是要存在一个被指向的对象,故而,静态方法、静态类中并无this关键字可使用,因为它们不需要创建对象就可使用(关于static的特点,之后会写到)。
作用:this可用于区别成员变量和局部变量,解决重名问题,其依据就近原则。
System.out.println(age);
System.out.print(this.age);
//第一个println优先输出局部变量中的age,若不存在,则输出成员变量中的age
注:this是可省略不写的,但变量的调用规则同上,且,
this关键字也可用于在构造函数中调用其他的构造函数,但是只能定义在构造函数的第一行,因为初始化动作要先执行:
public Animal() {
System.out.println("执行无参构造函数");
}
public Animal(String AName, int AAge, String AType) {
//调用其他构造函数时,只能定义在构造函数的第一行,因为初始化动作要先执行
this();
this.AName = AName;
this.AAge = AAge;
this.AType = AType;
}
测试方法:
public class Demo {
@Test
public void test1(){
Animal animal=new Animal("Mike",3,"Dog");
}
}
运行结果:
事实上,此时程序运行流程是先使用无参构造函数创建对象,再在有参构造函数中为对象属性赋值。
this的内存原理
this的本质存放的是所在方法调用者的地址,它是一个引用、一个变量,保存了存储在JVM堆内存中Java对象的内部,每一个Java对象都有一个属于自己的this。
图中例子来自黑马程序员教程,感觉不太好,此处我自己写一个例子并使用断点进行调试:
在实体类Animal中新增方法:
public void show(){
System.out.println("正在执行show方法...");
System.out.println("我在show方法中输出this:"+this);
System.out.println("show方法执行结束...");
}
进行单元测试:
public class Demo {
@Test
public void test1(){
Animal animal=new Animal("Mike",3,"Dog");
System.out.println("此时执行的是test1方法,而调用对象由jar包junit来创建(因为若使用static修饰的main,则不存在this关键字)...");
System.out.println("此时this为:"+this);
System.out.println("即将转到show方法内,注意,调用show方法的对象是animal...");
animal.show();
System.out.println("回到test1方法,我们输出animal对象来获取其引用,即可发现其引用就是在show方法执行过程中this的值:"+animal);
System.out.println("show方法执行结束,回到test1方法后,this的值又回到:"+this);
}
}
输出结果:
此时执行的是test1方法,而调用对象由jar包junit来创建(因为若使用static修饰的main,则不存在this关键字)...
此时this为:org.example.SimpleCode.Demo@4cdbe50f
即将转到show方法内,注意,调用show方法的对象是animal...
正在执行show方法...
我在show方法中输出this:org.example.polo.Animal@7e0babb1
show方法执行结束...
回到test1方法,我们输出animal对象来获取其引用,即可发现其引用就是在show方法执行过程中this的值:org.example.polo.Animal@7e0babb1
show方法执行结束,回到test1方法后,this的值又回到:org.example.SimpleCode.Demo@4cdbe50f
进程已结束,退出代码0
断点调试进行佐证:
Demo中:
Animal中:
进行断点调试:
①处于Demo类中时:
可见,此时this指向的Demo类的一个对象(由Junit创建的实例对象来调用test1方法)的引用,随后使用"步过(F8)"进入到Animal中。
②处于animal对象中时
可见,此时this保存的是对象animal的引用。
③继续使用步过回到test1()方法中:
可见,此时this为Demo实例对象的引用,继续步过到程序执行结束。
④处于Junit包中类对象实例中时:
可见,创建Demo的实例化对象的是JUnit4IdeaTestRunner类的对象。
注意:上图中出现的this并非是同一个this,而是不同对象中的不同this。
包、包路径及其目录结构
包主要用来对类和接口进行分类。当开发 Java 程序时,可能编写成百上千的类,因此很有必要对类和接口进行分类。在进行打包之后,可防止程序在多个地方同时出现相同的名字二产生的冲突,此时就可通过包名进行区分。
事实上,打包就是新建了一个文件夹,然后将需要打包的程序放在该文件夹内。
package:声明当前类的全类名(类的地址),必须放在第一行,可以有多层,每一层用"."隔开,事实上是多个文件夹的含义。
package org.example.SimpleCode;
查看文件夹结构:
命名规则:
通常,一个公司使用它互联网域名的颠倒形式来作为它的包名.例如:互联网域名是apple.com,所有的包名都以com.apple开头。包名中的每一个部分对应一个子目录。
例如:这个公司有一个com.apple.computers的包,这个包包含一个叫做Dell.java的源文件,那么相应的,应该有如下面的一连串子目录:
....\com\apple\computers\Dell.java
编译的时候,编译器为包中定义的每个类、接口等类型各创建一个不同的输出文件,输出文件的名字就是这个类型的名字,并加上.class作为扩展后缀。 例如:
// 文件名: Dell.java
package com.apple.computers;
public class Dell{
}
class Ups{
}
使用命令进行编译:
$javac -d . Dell.java
此时,.class文件的存放路径为:
.\com\apple\computers\Dell.class
.\com\apple\computers\Ups.class
具体命名规则:
个人项目命名:
indi :
个体项目(individual),指个人发起,但非自己独自完成的项目,可公开或私有项目,copyright主要属于发起者。
包名为“indi.发起者名.项目名.模块名……”
onem :
单人项目(one-man),推荐用indi,指个人发起,但非自己独自完成的项目,可公开或私有项目,copyright主要属于发起者。
包名为“onem.发起者名.项目名.模块名……”
pers :
个人项目(personal),指个人发起,独自完成,可分享的项目,copyright主要属于个人。
包名为“pers.个人名.项目名.模块名.……”
priv :
私有项目(private),指个人发起,独自完成,非公开的私人使用的项目,copyright属于个人。
包名为“priv.个人名.项目名.模块名.……”
团体项目命名:
team:
团队项目,指由团队发起,并由该团队开发的项目,copyright属于该团队所有。
包名为“team.团队名.项目名.模块名.……”
com :
公司项目,copyright由项目发起的公司所有。
包名为“com.公司名.项目名.模块名.……”
注,此处的具体命名规则转自文章:https://blog.csdn.net/qq_27512741/article/details/83239203
import语句:在 Java 中,如果给出一个完整的限定名,包括包名、类名,那么 Java 编译器就可以很容易地定位到源代码或者类。Import 语句就是用来提供一个合理的路径,使得编译器可以找到某个类。
例,下面的命令行将会命令编译器载入 java_installation/java/io 路径下的所有类:
import java.io.*;
事实上,在实际开发当中,包的命名是有约定俗成的规定的,通常格式为:
package
包路径
编译之后的.class文件应该和.java源文件一样,它们放置的目录应该跟包的名字对应起来。但是,并不要求.class文件的路径跟相应的.java的路径一样。你可以分开来安排源码和类的目录。
<path-one>\sources\com\apple\computers\Dell.java
<path-two>\classes\com\apple\computers\Dell.class
这样,你可以将你的类目录分享给其他的编程人员,而不用透露自己的源码。用这种方法管理源码和类文件可以让编译器和java虚拟机(JVM)可以找到你程序中使用的所有类型。
类目录的绝对路径叫做class path。设置在系统变量CLASSPATH中。编译器和java虚拟机通过将package名字加到class path后来构造.class文件的路径。
<path- two>\classes是class path,package名字是com.apple.computers,而编译器和JVM会在 <path-two>\classes\com\apple\compters中找.class文件。
一个class path可能会包含好几个路径。多路径应该用分隔符分开。默认情况下,编译器和JVM查找当前目录。JAR文件按包含Java平台相关的类,所以他们的目录默认放在了class path中。
注:class path在环境变量中定义。
源文件声明规则
当在一个源文件中定义多个类,并且还有import语句和package 语句时,要特别注意这些规则。
一个源文件中只能有一个 public 类
一个源文件可以有多个非public类
源文件的名称应该和public类的类名保持一致。例如:源文件中public类的类名是Employee,那么源文件应该命名为Employee.java。
如果一个类定义在某个包中,那么package语句应该在源文件的首行。
如果源文件包含import语句,那么应该放在package语句和类定义之间。如果没有package语句,那么import语句应该在源文件中最前面。
import语句和package语句对源文件中定义的所有类都有效。在同一源文件中,不能给不同的类不同的包声明。
继承
面向对象语言的三大特征:封装、继承、多态。
继承是所有 OOP 语言(面向对象语言)和 Java 语言不可缺少的组成部分。可以理解为一个对象从另一个对象获取属性、方法的过程。继承中最常用的两个关键字是extends和implements,这两个关键字分别用来声明并继承类和实现接口(接口放在后面写,这里只写类的继承)。如:
public class Dog extends Animal{
类的内容;
}
此时,将Dog类称之为子类(派生类),而将Animal类称为父类(基类、超类)。
事实上,可使用instanceof运算符来判断是否是子类:
@Test
public void test1(){
Dog dog=new Dog();
if(dog instanceof Animal){
System.out.println("dog 是 Animal的子类实例对象");
}
}
继承的好处:
可将多个子类中重复的代码抽取到父类中,提高代码的可用性。
子类可在父类的基础上增加其他的功能,使子类更强大。
注意:在 Java 中,类的继承是单一继承,也就是说,一个子类只能拥有一个父类,这与C++有很大区别。
关键字super及其原理分析
事实上,在创建子类的实例时,父类的实例或被隐式创建,而super关键字就用来保存父类实例的引用,故而,与前文中的this一样,super不能在静态类、静态方法中使用。
作用:事实上,当子类的构造方法第一行没有super时,会JVM会生成一个默认的super语句,用来调用父类的无参构造,而若想要调用父类的有参构造,可在子类构造方法第一行使用super来完成,且,子类中可通过super来调用父类成员(包括实例方法、静态方法)。
注意:子类中的super为什么必须要放在第一行?因为子类的实例化依赖于父类的实例化,在构建子类时,必须要有父类实例,只能有了父类的实例,子类才能够初始化自己。就好像人类世界里,都是要先有父亲,再有孩子一样。
例:
//Animal.java中
public Animal(String AName, int AAge, String AType) {
this.AName = AName;
this.AAge = AAge;
this.AType = AType;
System.out.println("使用了Animal的有参构造");
System.out.println("创建的Animal对象的this值为:"+this);
}
//Dog.java
package org.example.polo;
/**
* @author 悟兰因
* @version 2023
* @date 2023/1/7 11:39
* @package org.example.polo
*/
public class Dog extends Animal{
private String DHobby;
private float DWeight;
public Dog(){
}
public Dog(String AName, int AAge, String AType, String DHobby, float DWeight) {
super(AName, AAge, AType);
this.DHobby = DHobby;
this.DWeight = DWeight;
}
public void getSuper(){
System.out.println("正在执行Dog类中的getSuper方法,此时super的值为:"+super.toString());
}
}
//Demo.java
package org.example.SimpleCode;
import org.example.polo.Dog;
import org.junit.Test;
public class Demo {
@Test
public void test1(){
Dog dog=new Dog("Mike",3,"Dog","Running",30);
dog.getSuper();
}
}
程序运行结果:
使用了Animal的有参构造
创建的Animal对象的this值为:org.example.polo.Dog@66d33a
正在执行Dog类中的getSuper方法,此时super的值为:org.example.polo.Dog@66d33a
可见,创建Dog对象时会隐式创建其父类对象,且,super关键字会保存该父类对象的引用(即等于父类对象中this的值)。
继承的特点:
成员访问特点:就近原则
即:先在局部位置找,再去本类成员位置找,再去父类成员位置找,依次逐级向上,当遇到重名的成员变量时,可以使用this和super。
构造方法的特点
父类中的构造方法不能被子类继承(不能直接使用,需要使用super,即,使用父类对象来调用)
子类中所有的构造方法都默认先访问父类的无参构造(给父类属性进行默认初始化并完成父类数据空间的初始化,事实上,子类构造方法的第一行语句默认都是:super(),不写也存在,JVM会自动加上,且,必须在第一行,但若是想要调用父类的有参构造,必须手动写写super进行调用),再执行自己构造方法。
继承的内存分析
误区:
父类私有的东西,子类就无法继承。
父类非私有的东西,就会被子类所继承。
内存分析
在父类中有三种类的成员,分别是:构造方法、成员方法、成员方法。
构造方法:不管是什么修饰符,都不能继承。
成员变量:不管是什么修饰符,都能继承(不代表可以调用)。
成员方法:非private则可继承,private则不可继承。
成员变量内存图:
①没有private修饰时
子类被使用时,其父类的字节码文件也会被加载(Object也会被加载)。此时,堆内存中的子类对象的空间会被划分为两块,一块存放子类特有的成员,另一块存放其所继承父类的成员,并赋予初值。
②有private修饰时
对于private成员,所创建的子类对象当中仍会有,但是在进行修改时因发现是private修饰,故而修改失败,故,此处仍为所赋予的初值null。
③成员方法内存图
事实上,类会有自己的虚方法表,其中所包含的是非private、非static、非final方法,对于子类,其会继承父类的虚方法表到自己的虚方法表当中,使得调用时可以直接找到其地址,而不需要去父类中进行寻找。
例:
方法重写与@Override
当父类的方法不能满足子类现在的需求时,需要进行方法的重写。
在继承体系中,子类出现了和父类一样的方法声明,则称这个子类方法是重写的方法。
注意:方法重写!=方法重载
方法重写的本质:虚方法表中方法的覆盖。
注意:方法的重写应属于多态范围,这里先写了。
方法重写注解:@overload
注:注释是给开发者看的,而注写是给开发者和虚拟机看的,虚拟机可以通过注解辨别该方法,并检查是否书写正确(关于注解的原理,之后会写)。
@Override是放在重写后的方法上,可用来校验子类重写时语法是否正确(注意是子类,普通的重载用不了)
加上注解后如果有红色波浪线,则表示语法错误
建议重写的方法都加上@Override注解,代码安全、优雅
重写的要求:
例:
//Animal.java
public void say(){
System.out.println("I am an animal!");
}
//Dog.java
@Override
public void say(){
System.out.println("I am a dog!");
}
在IDEA中可用按钮在重写方法与被重写方法间跳转:
权限修饰符
用来控制一个成员能够被访问的范围,可以用来修饰成员变量、方法、构造方法、内部类。
实际开发中,一般只用private和public,即,成员变量私有和方法公开。
final与static
final
final是Java中的一个关键字,用来表示最终的、不可改变的,修饰时有以下特点:
方法:表名该方法是最终方法,不能被重写(可被重载)。
类:表名该类是最终类,不能被继承。
实例变量:一般情况下,实例变量若未被赋值,系统会赋默认值,而对于final修饰的实例变量,系统不负责赋默认值,要求程序员手动赋值,且只能赋一次(包括在变量后赋值、使用构造方法赋值等)。
@Test
public void test1(){
final int a;
System.out.println(a);
//报错:变量 'a' 可能尚未初始化
}
静态变量:此时即为static和final共同修饰,存储在方法区内,节省空间。
引用:此时该引用永远只能指向被赋值的对象,但对象内部的数据是可更改的(即,保存的永远是该对象的引用)。
事实上,String之所以不可变(只能改变变量名指向的对象而不能对对象本身进行修改),就是因为底层使用了final(注意,是字符数组使用final,而不是变量使用了final),查看String部分源码:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence,
Constable, ConstantDesc {
/**
* The value is used for character storage.
*
* @implNote This field is trusted by the VM, and is a subject to
* constant folding if String instance is constant. Overwriting this
* field after construction will cause problems.
*
* Additionally, it is marked with {@link Stable} to trust the contents
* of the array. No other facility in JDK provides this functionality (yet).
* {@link Stable} is safe here, because value is never null.
*/
@Stable
private final byte[] value;
...
}
static
static是Java中的一个关键字,表示"静态"的含义。修饰时有以下特点:
变量:称为静态变量。
事实上,类的成员变量根据是否由static修饰分为静态变量(亦称类变量)和实例变量。
静态变量:类的内部中任何方法都可直接进行访问,在其他类中可通过类名进行访问而不需要创建相关的实例化对象,在运行时,JVM只为静态变量分配一次内存,加载类的过程中就完成了内存的分配。
实例变量:每创建一个实例,JVM就会为实例变量分配一次内存,在类的内部可在非静态方法中直接进行访问,而在本类的静态方法或其他类中则需要通过类的实例化对象进行访问。
使用静态变量的作用:
静态变量可以被类的所有实例共享,因此静态变量可以作为实例之间的共享数据,增加实例之间的交互性。
如果类的所有实例都包含一个相同的常量属性,则可以把这个属性定义为静态常量类型,从而节省内存空间。例如,在类中定义一个静态常量 PI。
例:
public class Demo{
@Test
public void test1(){
A a1=new A();
System.out.println("开始时,a的值为:"+ a1.getA());
A a2=new A();
a2.setA(2);
System.out.println("使用a2修改静态变量成员a的值后,通过a1来获取a:"+a1.getA());
}
}
class A{
private static int a=1;
public void setA(int b){
a=b;
}
public int getA(){
return a;
}
}
运行结果:
开始时,a的值为:1
使用a2修改静态变量成员a的值后,通过a1来获取a:2
方法:称为静态方法。
与变量一样,方法也可依据是否被static修饰而分为静态方法和实例方法。
静态方法:用static,属于类而不属于类的对象,它通过类直接被调用,而无需创建类的对象,也正因如此,方法无法使用this,其子类无法使用super关键字,且,静态方法中亦可直接通过类名访问其他的静态变量和静态方法。
实例方法:需要创建相应的类对象后才可使用,而在方法内部可直接访问所属类的静态变量、静态方法、实例方法、实例变量。
例:
public class Demo{
@Test
public void test1(){
A.say2();
A a=new A();
a.say1();
}
}
class A{
public void say1(){
System.out.println("Hello!");
}
public static void say2(){
System.out.println("Hi!");
}
}
类:称为静态类,其只能作为内部类使用,若在外部类中声明static,则编译不会通过。
静态内部类:跟静态方法一样,只能访问外部类静态成员变量和方法,不能访问非静态方法和属性(这是因为外部类的非静态方法和属性需要先加载对象才能被加载)。可以声明普通成员变量和方法,而普通内部类不能声明static成员变量和方法,且静态内部类,可以单独初始化。
普通内部类:可以访问任意外部类的成员变量和方法。
public class Demo{
private static int a=1;
private int b=2;
static class A{
public void showA(){
Demo demo=new Demo();
System.out.println("能直接访问a:"+a+",而不能直接访问b"+demo.b);
//这是因为类A被加载时(JVM率先加载静态成员)外部类并不会被加载
}
}
class B{
public void showB(){
System.out.println("能直接访问a:"+a+",也直接访问b"+b);
//这是因为类B被加载时外部类也会被加载
}
public void getSuperAndThis(){
System.out.println("Super为:"+super.toString()+"\r\n"+"This为:"+this);
}
}
@Test
public void test1(){
B b=new B();
b.getSuperAndThis();
}
}
输出:
Super为:org.example.SimpleCode.Demo$B@2c8d66b2
This为:org.example.SimpleCode.Demo$B@2c8d66b2
注意,内部类与外部类的关系并非父子。
分析字节码文件:
Hello.java
public class Hello{
public static int a=1;
public int b=2;
public void show(){
System.out.println("Hello World");
}
}
使用终端打开:
PS C:\Users\lenovo\Desktop> javac Hello.java
PS C:\Users\lenovo\Desktop> javap -v Hello
Classfile /C:/Users/lenovo/Desktop/Hello.class
Last modified 2023年1月7日; size 501 bytes
SHA-256 checksum f8f0604ebcb69a3538770a889541b53a3e80e44afbedc4d5fdeddacfa9d5ec32
Compiled from "Hello.java"
public class Hello
minor version: 0
major version: 62
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #8 // Hello
super_class: #2 // java/lang/Object
interfaces: 0, fields: 2, methods: 3, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Fieldref #8.#9 // Hello.b:I
#8 = Class #10 // Hello
#9 = NameAndType #11:#12 // b:I
#10 = Utf8 Hello
#11 = Utf8 b
#12 = Utf8 I
#13 = Fieldref #14.#15 // java/lang/System.out:Ljava/io/PrintStream;
#14 = Class #16 // java/lang/System
#15 = NameAndType #17:#18 // out:Ljava/io/PrintStream;
#16 = Utf8 java/lang/System
#17 = Utf8 out
#18 = Utf8 Ljava/io/PrintStream;
#19 = String #20 // Hello World
#20 = Utf8 Hello World
#21 = Methodref #22.#23 // java/io/PrintStream.println:(Ljava/lang/String;)V
#22 = Class #24 // java/io/PrintStream
#23 = NameAndType #25:#26 // println:(Ljava/lang/String;)V
#24 = Utf8 java/io/PrintStream
#25 = Utf8 println
#26 = Utf8 (Ljava/lang/String;)V
#27 = Fieldref #8.#28 // Hello.a:I
#28 = NameAndType #29:#12 // a:I
#29 = Utf8 a
#30 = Utf8 Code
#31 = Utf8 LineNumberTable
#32 = Utf8 show
#33 = Utf8 <clinit>
#34 = Utf8 SourceFile
#35 = Utf8 Hello.java
{
public static int a;
descriptor: I
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
public int b;
descriptor: I
flags: (0x0001) ACC_PUBLIC
public Hello();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_2
6: putfield #7 // Field b:I
9: return
LineNumberTable:
line 1: 0
line 3: 4
public void show();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #13 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #19 // String Hello World
5: invokevirtual #21 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 5: 0
line 6: 8
static {};
descriptor: ()V
flags: (0x0008) ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_1
1: putstatic #27 // Field a:I
4: return
LineNumberTable:
line 2: 0
}
SourceFile: "Hello.java"
可见,静态的成员变量a会被加载到专用的ACC_STATIC寄存器当中。
代码块
在Java中,用"{}"括起来的代码称为代码块。
代码块的分类:
根据位置和声明不同,可以分为局部代码块,构造代码块,静态代码块,同步代码块
局部代码块:在方法中出现,顺序执行,用于限定变量生命周期,及早释放,提高内存利用率。
构造代码块(初始化块):在类中方法外出现,随着类的加载而被加载。
静态代码块:一般用于加载驱动,和.class一起被加载进内存。
public class Demo {
public static void main(String[] args) {
System.out.println("我是主方法");
//创建对象
question s=new question();
question s1=new question("Hello"); //主方法,在主方法类中的静态代码块执行之后执行
{
System.out.println("我是主方法中的局部代码块");
}
}
static {
System.out.println("我是主方法类中的静态代码块"); //主方法类中的静态代码块优先于主方法执行
}
}
class question {
private String name;
//无参构造
public question() {
System.out.println("我是无参构造");
}
//有参构造
public question(String name) {
this.name=name;
System.out.println("我是有参构造");
}
{
System.out.println("我是question类内部的构造代码块"); //随着对象的创建而加载,对象创建一次,构造代码块执行一次
}
static {
System.out.println("我是question类内部的静态代码块"); //静态代码块,随着类的加载而加载,并且只执行一次
}
}
运行结果:
我是主方法类中的静态代码块
我是主方法
我是question类内部的静态代码块
我是question类内部的构造代码块
我是无参构造
我是question类内部的构造代码块
我是有参构造
我是主方法中的局部代码块
多态
多态是同一个行为具有多个不同表现形式或形态的能力。多态性是对象多种表现形式的体现。
比如我们说"宠物"这个对象,它就有很多不同的表达或实现,比如有小猫、小狗、蜥蜴等等。那么我到宠物店说"请给我一只宠物",服务员给我小猫、小狗或者蜥蜴都可以,我们就说"宠物"这个对象就具备多态性。
表现形式:
父类类型 对象名称=子类对象;
多态的实现方式
重写(Overload,在继承中已写过了)
抽象类和抽象方法
接口
多态的好处:
使用父类型作为参数,可以接收所有子类对象,体现多态的扩展性和便利,例:
Person person=new Student();
多态形式下,右边对象可以实现解耦合,便于扩展和维护,例:
//希望Student工作:
Person p=new Student();
p.work();
//希望Teacher工作:
Person p=new Teacher();
p.work();
//而不需要对Person类中的work方法进行覆写,实现解耦合
抽象类和抽象方法
抽象方法:若想设计这样一个类,该类包含一个特别的成员方法,该方法的具体实现由它的子类确定,那么你可以在父类中声明该方法为抽象方法。
定义格式:
public abstract 返回值类型 方法名(参数列表);
抽象方法只包含一个方法名,并无方法体,方法体由继承的子类实现。
抽象方法必须为public或者protected(因为如果为private,则不能被子类继承,子类便无法实现该方法),缺省情况下默认为public。
抽象类:如果一个类包含抽象方法,那么该类必须是抽象类,要用abstract修饰。
定义格式:
public abstract class 类名{}
抽象类不能实例化,即,不能创建对象,但可以有构造方法,因为抽象类中会存在一些属性,构造方法可用来为属性进行初始化,且,其执行原则与普通类无异,在子类对象实例化时,依旧会先执行父类构造再执行子类构造。
抽象类中不一定有抽象方法,但有抽象方法的一定是抽象类。
任何子类必须重写父类的全部抽象方法,或者声明自身为抽象类(此时,分类和子类仍不能用来实例化对象)。
类的继承原则不变,一个子类继承一个抽象类后既不能再继承其他类。
抽象类的权限修饰符可不写,此时为default,此外只能使用public(要注意一个源文件只能有一个public类的原则)和protected。
例:
public class Demo {
abstract class Person{
//声明一个抽象类
private String name;
public Person() {
}
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
abstract void say();
}
class Man extends Person{
//继承抽象类
private final String gender="male";
private int age;
public Man() {
}
public Man(String name,int age) {
super(name);
this.age = age;
}
@Override
void say() {
System.out.println("I'm a man,My name is "+super.getName()+",I'm "+age+" now!");
}
}
@Test
public void test1(){
Man man=new Man("Mike",18);
man.say();
}
}
注:IDEA在重写抽象类方法中可使用Ctrl+O,此时会给出函数列表并自动加上注解@Override。
总结:所有的子类如果要想正常的完成操作,必须按照指定的方法进行覆写才可以,而这个时候抽象类所起的功能就是一个类定义模板的功能。
接口
接口(英文:Interface),在JAVA编程语言中是一个抽象类型,是抽象方法的集合,接口通常以interface来声明。一个类通过继承接口的方式,从而来继承接口的抽象方法。
接口代表规则,是行为的抽象,想让哪个类拥有一个行为,就让这个类实现对应的接口即可。
接口并不是类,编写接口的方式和类很相似,但是它们属于不同的概念。类描述对象的属性和方法。接口则包含类要实现的方法。
除非实现接口的类是抽象类,否则该类要定义接口中的所有方法。
接口无法被实例化,但是可以被实现。一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类。另外,在Java中,接口类型可用来声明一个变量,他们可以成为一个空指针,或是被绑定在一个以此接口实现的对象。
接口中成员的特点:
成员变量:只能是常量,默认修饰符为:public static final。
构造方法:没有。
成员方法:只能是抽象方法,默认修饰符为:public abstract。(JDK7以前,接口中只能定义抽象方法,JDK8中,接口中可以定义有方法体的方法,JDK9中,接口可以定义私有方法)
接口和类之间的关系:
类和类之间的关系:继承关系、只能单继承,不能多继承,但是可以多层继承。
接口和类的关系:实现关系,可以单实现也可以多实现,还可以在继承一个类的同时实现多个接口,若接口中有重名的方法,则只需要重写一次即可。
接口和接口之间的关系:继承关系,可以单继承,也可以多继承。
JDK8开始接口中新增的方法:
JDK7以前:接口中只能定义抽象方法。
JDK8新特性:接口中可以定义有方法体的方法(默认、静态)。这是为了提升接口升级时的兼容性(若接口中新增了方法,则JDK7必须所有实现类都立即重写,否则会报错,JDK8的新特性使得此时实现类不需要立马修改,等以后用到某个规则再进行重写即可)。即:
允许在接口中定义默认方法,需要使用关键字default修饰,作用:解决接口升级问题。
接口中默认方法的定义格式:public default 返回值类型 方法名(参数列表){}
接口中默认方法的注意事项:
默认方法不再是抽象方法,所以不强制被重写,重写的时候不能再加上default关键字。
public可以省略,但default不可省略。
若实现了多个接口,多个接口中存在相同名字的默认方法,子类就必须对该方法进行重写。
允许在接口中定义静态方法,使用static修饰。
接口中静态方法的定义格式:public static 返回值类型 方法名(参数列表){}
接口中静态方法的注意事项:
静态方法只能通过接口名调用,不能通过实现类名或者对象名调用。
public可以省略,但static不可省略。
JDK9新特性:接口中可以定义私有方法(private修饰),使得此方法只能为接口提供服务,不能被外部类访问。定义格式:
格式1(普通私有方法,给默认方法服务)
private 返回值类型 方法名(参数列表){}
范例:private void show(){}
格式2(静态私有方法,给静态方法服务)
private static 返回值类型 方法名(参数列表){}
范例:private static void show(){}
定义格式:
interface 接口名{
}
继承格式:
权限修饰符 class 类名 implements 接口名1,接口名2,...{
}
例:
interface Swimming{
//声明一个接口
void Swim();
};
abstract class Animal{
//声明一个抽象类
public abstract void Say();
//声明一个抽象方法
}
class Dog extends Animal implements Swimming{
@Override
public void Say(){
//实现抽象类的抽象方法(可不实现,但Dog类要声明为抽象类)
System.out.println("I'm a dog!");
}
@Override
public void Swim(){
//实现接口中的抽象方法(一定要实现)
System.out.println("I can Swim!");
}
}
多态中成员的调用原则
多态的弊端:
不能调用子类的特有功能。
例:
public class Demo {
public static void main(String[] args){
Animal animal=new Dog();
animal.
}
}
class Animal{
public void eat(){
System.out.println("动物在吃东西");
}
}
class Dog extends Animal{
@Override
public void eat(){
System.out.println("狗在吃东西");
}
public void lookHome(){
System.out.println("狗在看家");
}
}
可见,对象a并不能调用lookHome()方法。
这是因为调用成员方法时,编译看左边,运行看右边,则,在编译时会先检查左边的父类中有没有这个方法,如果没有就直接报错。
解决方案:
变回子类类型即可,即:
Dog d=a;
//这是不行的,不能给范围小的赋值范围大的
Dog d=(Dog)a;
//正确写法,即先将范围大的强制转化为范围小的再进行赋值(这一点与基本数据类型中数据的转换是一致的)
若不知道具体类型,可以使用:
if(a instanceof Dog){
Dog d=(Dog)a;
d.lookHome();
}else if(a instanceof Cat){
Cat c=(Cat)a;
c.catchMouse();
}else{
System.out.println("没有这个类型,无法转换")
}
可以省略写法:
if(a instanceof Dog d){
d.lookHome();
}else if(a instanceof Cat c){
c.catchMouse();
}else{
System.out.println("没有这个类型,无法转换")
}
//先判断a是否为Dog类型,若是,则强转为Dog类型,转换之后变量名为d
多态调用成员的特点:
变量调用:编译看左边,运行也看左边
编译看左边:javac编译代码时,会看左边的定义对象时所使用的父类中有没有这个变量,若有,则编译成功,若无,则编译失败。
运行看左边:java运行代码时,实际获取的就是左边的定义对象时所使用的父类中成员变量的值。
例:
Animal a=new Dog();
//之后都是使用a去调用变量,而a是animal类型的,故而默认都会从Animal这个类中去寻找
例:
package com.example;
public class Demo {
public static void main(String[] args){
Person person1=new Person();
Person person2=new Student();
System.out.println(person1.getName());
System.out.println(person2.getName());
}
}
(此处,person类中定义了变量name="person",student类中定义了变量name="student",可见,输出的都是person中的变量)
方法调用:编译看左边,运行看右边
编译看左边:javac编译代码的时候,会看左边的定义对象时所使用的父类中有没有这个方法,若有,则编译成功,若无,则编译失败。
运行看右边:java运行代码时,实际上先寻找的是右边new的子类中的方法,即先判断子类是否对父类中的方法进行了覆写。
父类和子类的字节码文件都会被加载到方法区中(这里省略了Object类),且,会进行方法的覆写,在执行sout(a.name)时,会先在堆内存中找到该变量,先查看到父类中有name这个变量时才能编译成功,否则编译失败(若写的是:Dog d=new dog,则会先去子类的空间中寻找name)。执行a.show()时,编译会在父类中先去寻找是否有show()方法,若找不到,则编译失败,运行时则是子类中的show(),因为子类进行了覆写。类
Java高级语法
泛型
泛型,即,"类型参数化",就是将原本应赋予的类型设置为一个参数,而参数是可由开发人员自己写入的,使用了泛型之后,数据类型相当于一个变量,可在调用时进行设置,这就大大提高了代码的重用性。
泛型就相当于是一个特殊的数据类型(可理解为相当于是通配符"*"),可用在类、方法、接口中,此时即为,泛型类、泛型方法、泛型接口。
事实上,泛型是JDK5才引入的,且,泛型提供了编译时类型安全监测机制,该机制运行程序员在编译时检测到非法的数据类型,且,泛型相关的所有强制转换都是自动和隐式的。
需要注意的是,传入的数据类型必须是引用数据类型,基本数据类型则使用其包装类。
格式:
<数据类型>
泛型的好处:
统一数据类型。
把运行时期的问题提前到了编译期间,避免了强制类型转换可能出现的异常,因为,在编译阶段类型就能确定下来。
例(关于集合家族后面会额外写):
public class Demo {
public static void main(String[] args){
//没有泛型时,集合可存储任意数据类型的元素,但是在获取数据时无法使用它的特有行为(因为不知道获取到数据的类型,自然无法调用相关的方法、属性等),故而,推出了泛型机制来在添加数据时就统一类型,同时也省去了强转
ArrayList arrayList=new ArrayList();
arrayList.add(123);
arrayList.add("aaa");
arrayList.add(new Student("张三",18));
Iterator iterator=arrayList.iterator();
while(iterator.hasNext()){
Object object= iterator.next();
//没有泛型约束时,默认是Object类型,但是,由于多态的弊端在于无法访问子类的特有功能,使得此处不能用object调用对象的功能,且,由存储的数据类型不同,故而,不能进行全体数据强转
System.out.println(object);
}
}
}
注:不写不传入数据类型,则默认为Object类型,是所有类的超类。
泛型的编译时安全监测机制:
使用了泛型限制数据类型之后,写入以下代码编译器会自动报错,且此时运行程序也会显示编译不通过,这就是泛型在编译时安全监测机制(相当于告诉编译器接收的对象应是什么类型,使得编译器在编译器就会自动检查)。
public class Demo {
@Test
public void test1(){
ArrayList<String> arrayList=new ArrayList<>();
arrayList.add("Hi");
arrayList.add(1);
//IDEA报错:'java.util.ArrayList' 中的 'add(java.lang.String)' 无法应用于 '(int)'
}
}
但是,泛型的作用也仅限于此,因为事实上,在编译之后程序会采取去泛型的措施,也就是说,Java中的泛型实际上是"伪泛型",编译完成后就会将泛型相关信息自动擦除,并在对象进入和离开方法的边界处添加类型检查和类型转换的方法。
下面即为佐证:
①未使用泛型的程序
package org.example.SimpleCode;
import java.util.ArrayList;
public class Demo {
public static void main(String[] args){
ArrayList arrayList=new ArrayList();
//此时的警告即为:"形参化类 'ArrayList' 的原始使用",即是指未传入具体数据类型作为参数
arrayList.add("Hello");
//此时警告为:"作为原始类型 'java.util.ArrayList' 的成员对 'add(E)' 的未检查的调用",未指定数据类型,自然没有相关的检查
arrayList.add("Hi");
}
}
我们在IDEA的目录中事实上是可以找到它的.class文件所在位置的(我创建的是Maven工程),就在target目录(用于存放程序运行的结果,可能需要手动clean)下:
右键进入该目录(此时打开只会得到反编译的结果),并在终端打开,使用命令来打开字节码文件:
PS D:\IntelliJ IDEA\Demo\target\classes\org\example\SimpleCode> javap -v Demo
警告: 文件 .\Demo.class 不包含类 Demo
Classfile /D:/IntelliJ IDEA/Demo/target/classes/org/example/SimpleCode/Demo.class
Last modified 2023年1月8日; size 570 bytes
SHA-256 checksum 9ca8a4042895afb076b84fe40ec60a47d9ee9290f3a282c1314de725001ba646
Compiled from "Demo.java"
public class org.example.SimpleCode.Demo
minor version: 0
major version: 58
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #18 // org/example/SimpleCode/Demo
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Class #8 // java/util/ArrayList
#8 = Utf8 java/util/ArrayList
#9 = Methodref #7.#3 // java/util/ArrayList."<init>":()V
#10 = String #11 // Hello
#11 = Utf8 Hello
#12 = Methodref #7.#13 // java/util/ArrayList.add:(Ljava/lang/Object;)Z
#13 = NameAndType #14:#15 // add:(Ljava/lang/Object;)Z
#14 = Utf8 add
#15 = Utf8 (Ljava/lang/Object;)Z
#16 = String #17 // Hi
#17 = Utf8 Hi
#18 = Class #19 // org/example/SimpleCode/Demo
#19 = Utf8 org/example/SimpleCode/Demo
#20 = Utf8 Code
#21 = Utf8 LineNumberTable
#22 = Utf8 LocalVariableTable
#23 = Utf8 this
#24 = Utf8 Lorg/example/SimpleCode/Demo;
#25 = Utf8 main
#26 = Utf8 ([Ljava/lang/String;)V
#27 = Utf8 args
#28 = Utf8 [Ljava/lang/String;
#29 = Utf8 arrayList
#30 = Utf8 Ljava/util/ArrayList;
#31 = Utf8 SourceFile
#32 = Utf8 Demo.java
{
public org.example.SimpleCode.Demo();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/example/SimpleCode/Demo;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #7 // class java/util/ArrayList
3: dup
4: invokespecial #9 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: ldc #10 // String Hello
11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
14: pop
15: aload_1
16: ldc #16 // String Hi
18: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
21: pop
22: return
LineNumberTable:
line 7: 0
line 9: 8
line 11: 15
line 12: 22
LocalVariableTable:
Start Length Slot Name Signature
0 23 0 args [Ljava/lang/String;
8 15 1 arrayList Ljava/util/ArrayList;
}
SourceFile: "Demo.java"
②使用泛型的程序
package org.example.SimpleCode;
import java.util.ArrayList;
public class Demo {
public static void main(String[] args){
ArrayList<String> arrayList= new ArrayList<>();
arrayList.add("Hello");
arrayList.add("Hi");
}
}
得到字节码文件:
PS D:\IntelliJ IDEA\Demo\target\classes\org\example\SimpleCode> javap -v Demo
警告: 文件 .\Demo.class 不包含类 Demo
Classfile /D:/IntelliJ IDEA/Demo/target/classes/org/example/SimpleCode/Demo.class
Last modified 2023年1月8日; size 657 bytes
SHA-256 checksum a40d48f558385dec38d80aca8d105f454eac80040a937953c57a6afdf74d055a
Compiled from "Demo.java"
public class org.example.SimpleCode.Demo
minor version: 0
major version: 58
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #18 // org/example/SimpleCode/Demo
super_class: #2 // java/lang/Object
interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Class #8 // java/util/ArrayList
#8 = Utf8 java/util/ArrayList
#9 = Methodref #7.#3 // java/util/ArrayList."<init>":()V
#10 = String #11 // Hello
#11 = Utf8 Hello
#12 = Methodref #7.#13 // java/util/ArrayList.add:(Ljava/lang/Object;)Z
#13 = NameAndType #14:#15 // add:(Ljava/lang/Object;)Z
#14 = Utf8 add
#15 = Utf8 (Ljava/lang/Object;)Z
#16 = String #17 // Hi
#17 = Utf8 Hi
#18 = Class #19 // org/example/SimpleCode/Demo
#19 = Utf8 org/example/SimpleCode/Demo
#20 = Utf8 Code
#21 = Utf8 LineNumberTable
#22 = Utf8 LocalVariableTable
#23 = Utf8 this
#24 = Utf8 Lorg/example/SimpleCode/Demo;
#25 = Utf8 main
#26 = Utf8 ([Ljava/lang/String;)V
#27 = Utf8 args
#28 = Utf8 [Ljava/lang/String;
#29 = Utf8 arrayList
#30 = Utf8 Ljava/util/ArrayList;
#31 = Utf8 LocalVariableTypeTable
#32 = Utf8 Ljava/util/ArrayList<Ljava/lang/String;>;
#33 = Utf8 SourceFile
#34 = Utf8 Demo.java
{
public org.example.SimpleCode.Demo();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/example/SimpleCode/Demo;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #7 // class java/util/ArrayList
3: dup
4: invokespecial #9 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: ldc #10 // String Hello
11: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
14: pop
15: aload_1
16: ldc #16 // String Hi
18: invokevirtual #12 // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z
21: pop
22: return
LineNumberTable:
line 7: 0
line 8: 8
line 9: 15
line 10: 22
LocalVariableTable:
Start Length Slot Name Signature
0 23 0 args [Ljava/lang/String;
8 15 1 arrayList Ljava/util/ArrayList;
LocalVariableTypeTable:
Start Length Slot Name Signature
8 15 1 arrayList Ljava/util/ArrayList<Ljava/lang/String;>;
}
SourceFile: "Demo.java"
可见,已经进行了擦除。
泛型方法
在调用方法的时候指明泛型的具体类型。
常用的泛型参数名称:
E: Element (在集合中使用,因为集合中存放的是元素)
T:Type(Java 类)
K: Key(键)
V: Value(值)
N: Number(数值类型)
?: 表示不确定的java类型
语法格式:
权限修饰符 <T,E,...> 返回值类型(亦可是泛型) 方法名(参数列表(亦可是泛型)){
方法体
}
例:
public class Demo {
public <E> void printArray(E[] Array){
for (E element:Array) {
System.out.println(element);
}
}
@Test
public void test(){
Integer[] array=new Integer[]{1,2,3,4,5};
printArray(array);
}
}
注意:泛型方法与使用了泛型的方法是不一样的,如以下只是一个普通方法,只是它的参数是一个泛型(由泛型类传递而来),事实上,只有在权限修饰符后声明了泛型标识符的方法才是泛型方法:
class A<T>{
public void fun(T k){
System.out.println(k);
}
}
泛型方法与可变参数(记住前面有关可变参数的理解,它就是一个数组,此处即为一个数组中存放多种泛型,相当于一个未传递数据类型的泛型集合):
public class Demo {
public <E> void print(E ... args) {
for(E arg:args){
System.out.println(arg);
}
}
@Test
public void test() {
print(123,"ABC",3.14);
}
}
泛型方法与静态方法
静态方法无法访问类上定义的泛型(因为静态成员被调用时,类不一定初始化了对象),若静态方法操作的引用数据类型不确定时,必须将泛型定义在方法上,即,若静态方法要使用泛型,必须将静态方法也定义为泛型方法,如:
public class StaticGenerator<T> {
....
....
/**
* 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
* 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
* 如:public static void show(T t){..},此时编译器会提示错误信息:
"StaticGenerator cannot be refrenced from static context"
*/
public static <T> void show(T t){
}
}
//注意,show方法中的泛型是由参数传递而来,而非类
泛型类
当一个类中,某个变量的数据类型不确定时,就可使用带有泛型的类。通过泛型类可完成对一组类的操作对外开放相同的接口,如各种容器类,List、Set、Map等。
注:未传递时,默认为Object。
语法格式:
权限修饰符 class 类名<E,T...>{
}
//例:
public class ArrayList<E>{
}
//此处的E可以理解为一个变量,不是用来记录数据的,而是用来记录数据的类型的,事实上,其可以写为:T、E、K、V等.
例:
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
//key这个成员变量的类型为T,T的类型由外部指定
private T key;
public Generic(T key) {
//形参key的类型为T,T的类型由外部指定
//注意,这个是普通方法,而非泛型方法,后者的类型是传递参数时指定
this.key = key;
}
public T getKey(){
//普通方法,返回值类型为T,T的类型由外部指定
return key;
}
}
泛型接口
泛型接口与泛型类的定义基本相同,常被用于各种类的生产器中,如:
//定义一个泛型接口
public interface Generator<T> {
public T next();
}
例:
//定义一个泛型接口
/**
* 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
*/
class FruitGenerator<T> implements Generator<T>{
@Override
public T next() {
return null;
}
}
/**
* 传入泛型实参时:
* 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
* 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
* 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
* 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。
*/
public class FruitGenerator implements Generator<String> {
private String[] fruits = new String[]{"Apple", "Banana", "Pear"};
@Override
public String next() {
Random rand = new Random();
return fruits[rand.nextInt(3)];
}
}
泛型通配符
泛型通配符用于解决泛型之间引用传递时出现的继承关系,主要有以下三类:
无边界通配符:<?>,表示参数类型可以是任意类型。
固定上边界通配符:<T extends E>,表示参数类型T必须是E或E的子类。
固定下边界通配符:<T super E>,表示参数类型T必须是A或A的超类。
例:
//表示类型参数可以是任何类型
public class Apple<?>{}
//表示类型参数必须是A或者是A的子类
public class Apple<T extends A>{}
//表示类型参数必须是A或者是A的超类型
public class Apple<T supers A>{}
注解
注解,是JDK5引入的一种新特性,用于提供信息给编译器,使得编译器可直接进行相关处理。如果说,注释是给开发人员查看的的,那么,注解就是给开发人员和编译器共同查看的。
注解就像修饰符一样,使用时在其前面增加@符号,用于修饰包、类、构造方法、域、方法、参数以及局部变量的声明,这些信息被存在注解的“name=values”键值对中。注解不影响程序代码的运行,无论增加还是删除注解,代码都始终如一的执行。如果希望程序中的注解在运行时起到一定作用,需要通过配套的工具对注解中的信息进行访问和处理,这种工具统称为APT(Annotation Processing Tool,注解处理工具)。
APT注解处理工具负责提取注解中包含的元数据,并会根据这些元数据增加额外功能。注解中元数据的作用有以下三个方面:
编写文档——通过注解中标识的元数据可以生成doc文档
代码分析——通过注解中标识元数据对代码进行分析
编译检查——通过注解中标识的元数据,让编译器能够实现基本的编译检查,例如@Override重写
注解的本质
注:此处部分转载自文章https://www.cnblogs.com/hncboy/p/12208132.html
所有的注解本质上都是继承于Annotation(注解)接口,在定义注解时不仅仅需要继承该接口,还需要通过@interface来声明这是一个注解,查看Annotation源码:
package java.lang.annotation;
public interface Annotation {
boolean equals(Object obj);
int hashCode();
String toString();
Class<? extends Annotation> annotationType();
//用于获取注解的类型
}
来对比以下@interface定义注解和继承Annotation接口:
public @interface TestAnnotation1 {
}
public interface TestAnnotation2 extends Annotation {
}
使用javap进行编译:
// javap -c TestAnnotation1.class
Compiled from "TestAnnotation1.java"
public interface com.hncboy.corejava.annotation.TestAnnotation1 extends java.lang.annotation.Annotation {}
// javap -c TestAnnotation2.class
Compiled from "TestAnnotation2.java"
public interface com.hncboy.corejava.annotation.TestAnnotation2 extends java.lang.annotation.Annotation {}
可见,不管有没有写出继承Annotation接口的代码,注解都会继承Annotation接口称为一个特殊的接口,其具体实现类是Java运行时生成的动态代理类,而我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象$Proxy1,。通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler的invoke方法,该方法会从member这个Map索引处对应的值(即为注解的成员变量),而memberValues的来源是Java常量池。
虽然本质上都是继承Annotation接口,但即使接口可实现多继承,注解的定义仍不能使用继承关键字来实现,通过@interface定义注解后,该注解也不能继承其他注解或接口,即,不能再去继承别的类或是接口:
public @interface TestAnnotation1 {
}
/** 错误的定义,注解不能继承注解 */
@interface TestAnnotation2 extends TestAnnotation1 {
}
/** 错误的定义,注解不能继承接口 */
@interface TestAnnotation3 extends Annotation {
}
虽然不支持继承,但可使用组合注解来解决这一问题,如@SpringBootApplication注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
}
注解的种类
JDK自带注解
元注解
自定义注解
JDK自带注解
JDK自带的注解一共有5个:
@Override:用来标识重写方法,它说明被标注的方法重载了父类的方法,若使用了这种注解在一个没有覆盖父类的方法时,java编译器将以一个编译错误表示。
@Deprecated:当一个类型或类型成员被修饰后,编译器将不鼓励使用这个被标注的程序元素(声明这个元素已经"过时"了),即使继承或覆盖后的类或成员并不声明为@Deprecated,但编译器仍会报警。
@SuppressWarnings("deprecation"):忽略警告。
@SafeVarargs:JDK1.7出现,会出现堆污染,不常用。
@FunctionalInterface:JDK1.8出现,配置函数式编程Lambda表达式,不常用。
元注解
元注解是由java.lang.annotation提供的四种注解,专门注解其他注解,在自定义注解的时候,就需要使用到元注解:
@Documented:表示是否将注解信息添加在java文档中。
@Target:指明注解应用于什么地方,默认值为任何元素,可用的ElementType参数包括:
ElementType.CONSTRUCTOR:用于描述构造器。
ElementType.FIELD:成员变量、对象、属性。
ElementType.LOCAL_VARIABLE:用于描述局部变量。
ElementType.METHOD:用于描述方法。
ElementType.PACKAGE:用于描述包。
ElementType.PARAMETER:用于描述参数。
ElementType.TYPE:用于描述类、接口(包括注解类型)或enum类型。
@Retention:定义注解的生命周期。
RetentionPolicy.SOURCE:在编译阶段丢弃,声明后注解在编译结束之后就不再有任何意义,故它们不会写入字节码文件。@Override、@SuppressWarnings都属于这类注解。
RetentionPolicy.CLASS:在类加载时丢弃,在字节码文件的处理中有用,是注解默认的生命周期。
RetentionPolicy.RUNTIME:始终不会丢弃,运行期也保留该注解,因此可使用反射机制来获取此类注解的信息,一般自定义注解会声明此生命周期。
@Inherited:被修饰的注解,若作用于某个类上,其子类是可以继承该注解的,反之,若一个注解没有被@Inherited注解所修饰,则它的作用范围只能是当前类,子类并不能被继承。
自定义注解
定义规则:
参数成员只能用public修饰(这也是默认的)。
参数成员使用基本数据类型和String、Enum、Class、annotations等数据类型及其它们的数组。
要获取注解的信息只能通过反射机制来获取。
自定义注解中只有成员变量而无成员方法,且,成员变量的定义采用"无参数形式的方法"的定义方式来定义,用方法返回值作为成员变量类型,方法名作为变量名,无参数列表,使用default来声明默认值。
例:
注解BuyInfo.java
package org.example.polo;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface BuyInfo {
//订单编号
int id()default -1;
//商品枚举
enum goodSpecies{Apple,Banana,Orange}
//购买商品名称
goodSpecies goodName()default goodSpecies.Apple;
//成交价格
double Price()default 0;
}
注解CustomerGender.java
package org.example.polo;
public class Mike {
//顾客姓名
@CustomerName("Mike")
private String Name;
//顾客性别
@CustomerGender(gender = CustomerGender.Gender.Male)
private String Gender;
//顾客订单信息
@BuyInfo(id = 3,goodName = org.example.polo.BuyInfo.goodSpecies.Banana,Price = 20.5)
private String BuyInfo;
}
注解CustomerName.java
package org.example.polo;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CustomerName {
//顾客姓名默认为空字符串
String value() default "";
}
实体类Mike.java
package org.example.polo;
public class Mike {
//顾客姓名
@CustomerName("Mike")
private String Name;
//顾客性别
@CustomerGender(gender = CustomerGender.Gender.Male)
private String Gender;
//顾客订单信息
@BuyInfo(id = 3,goodName = org.example.polo.BuyInfo.goodSpecies.Banana,Price = 20.5)
private String BuyInfo;
}
工具类CustomerInfoUtil.java,用于通过反射获取注解的属性值
package org.example.polo;
import java.lang.reflect.Field;
/**
* @author 悟兰因
* @version 2023
* @date 2023/1/8 22:01
* @package org.example.polo
*/
public class CustomerInfoUtil {
public static void getFruitInfo(Class<?> clazz) {
String strCustomerName = "顾客姓名:";
String strCustomerGender = "顾客性别:";
String strBuyInfo = "订单信息:";
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(CustomerName.class)) {
CustomerName customername = (CustomerName) field.getAnnotation(CustomerName.class);
strCustomerName = strCustomerName + customername.value();
System.out.println(strCustomerName);
} else if (field.isAnnotationPresent(CustomerGender.class)) {
CustomerGender customerGender = (CustomerGender) field.getAnnotation(CustomerGender.class);
strCustomerGender = strCustomerGender + customerGender.gender().toString();
System.out.println(strCustomerGender);
} else if (field.isAnnotationPresent(BuyInfo.class)) {
BuyInfo buyInfo = (BuyInfo) field.getAnnotation(BuyInfo.class);
strBuyInfo = "订单编号:" + buyInfo.id() + ",商品名称:" + buyInfo.goodName() + ",成交价格:" + buyInfo.Price();
System.out.println(strBuyInfo);
}
}
}
}
测试代码:
package org.example.SimpleCode;
import org.example.polo.CustomerInfoUtil;
import org.example.polo.Mike;
import org.junit.Test;
public class Demo {
@Test
public void test() {
CustomerInfoUtil.getFruitInfo(Mike.class);
}
}
输出结果:
顾客姓名:Mike
顾客性别:Male
订单编号:3,商品名称:Banana,成交价格:20.5
进程已结束,退出代码0
反射
动态语言:在运行时可根据某些条件改变其自身结构的语言,如,C#、JavaScript、PHP、Python等。
静态语言:在运行时不可根据某些条件改变其自身结构的语言,如,Java、C、C++等。
注:Java不是动态语言,但可称之为是"准动态语言",即,Java有一定的动态性,可利用反射机制、字节码操作获得类似动态语言的特性,使得编程更加灵活。
概述
Reflection(反射),被视为是动态语言的关键,反射机制允许程序在执行期间借助Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。
事实上,加载完类后,在堆内存的方法区中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象包含了完整的类的结构信息,可通过这个对象看到类的结构。
正是由于这个对象就像一面镜子,透过这个镜子能看到类的结构,故而,称之为反射。
Java反射机制提供的功能:
在运行时判断任意一个对象所属的类。
在运行时构造任意一个类的对象。
在运行时判断任意一个类所具有的成员变量和方法。
在运行时获取泛型信息。
在运行时调用任意一个对象的成员变量和方法。
在运行时处理注解。
生成动态代理。
反射相关的主要API:
java.lang.Class:代表一个类
java.lang.reflect.Method:代表类的方法
java.lang.reflect.Field:代表类的成员变量
java.lang.reflect.Constructor:代表类的构造器
... ...
一个实例
将用Class来获取Person类的所有方法以及属性,包括私有成员与类构造函数。
创建Person.java
package org.example.polo;
public class Person {
private String name;
public int age;
public Person() {
}
private Person(String name){
this.name=name;
}
public Person(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;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public void show(){
System.out.println("Hello! I'm a Person");
}
private String showNation(String nation){
System.out.println("I'm com from "+nation);
return nation;
}
}
进行测试:
@Test
public void test4() throws Exception {
//1.通过反射,创建Person类的Class对象,并创建一个Person实例
Class clazz=Person.class;
//2.通过反射来获取Person的构造方法
Constructor constructor=clazz.getConstructor(String.class,int.class);
Object object=constructor.newInstance("Tom",12);
Person person=(Person)object;
System.out.println(person);
//3.通过反射,调用对象指定的属性
Field age=clazz.getDeclaredField("age");
age.set(person,10);
System.out.println(person);
//4.通过反射,调用对象指定的方法
Method show=clazz.getDeclaredMethod("show");
show.invoke(person);
System.out.println("------------------私有结构-----------------");
//5.通过反射可调用Person类的私有结构
//调用私有的构造器
Constructor constructor1=clazz.getDeclaredConstructor(String.class);
constructor1.setAccessible(true);
Person person1=(Person) constructor1.newInstance("Jerry");
System.out.println(person1);
//调用私有的属性
Field name=clazz.getDeclaredField("name");
name.setAccessible(true);
name.set(person1,"HanMeimei");
System.out.println(person1);
//调用私有方法
Method showNation=clazz.getDeclaredMethod("showNation",String.class);
showNation.setAccessible(true);
showNation.invoke(person1,"中国");
}
运行结果:
Person{name='Tom', age=12}
Person{name='Tom', age=10}
Hello! I'm a Person
------------------私有结构-----------------
Person{name='Jerry', age=0}
Person{name='HanMeimei', age=0}
I'm com from 中国
进程已结束,退出代码0
反射的本质
类的执行过程
加载:将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口(即,引用地址)。所有需要访问和使用类数据都只能通过这个Class对象,这个加载的过程需要类加载器(ClassLoader)参与。
链接:将Java类的二进制代码合并到JVM运行状态之中的过程。
验证:确保加载的类信息符合JVM规范。
准备:正式为类变量(static)分配内存并设置类变量默认初始化值的阶段,这些内存都将在方法区中进行分配。
解析:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
初始化:
执行类构造器<clinit>()方法的过程。类构造器<clinit>()方法是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的(类构造器是构造类信息的,不是构造该类对象的构造器)。
当初始化一个类的时候,若发现其父类还未进行初始化,则需要先触发其父类的初始化。
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步。
作用:将class文件字节码内容加载到内存中,并将这些静态数据转换为方法区的运行时的数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口。
类缓存:标准的JavaSE类加载器可按照要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存中)一端时间,可用JVM垃圾回收机制来回收这些对象。
关于java.lang.Class类的理解
程序经过javac.exe命令后,会生成一个或多个字节码文件(.class结尾)(此为类的编译),之后使用java.exe命令对某个字节码文件进行解释运行,相当于将某个字节码文件加载到内存中,此过程称为类的加载。而加载到内存中的类,称之为运行时的类,并作为Class的一个实例。
即,Class的一个实例对应着一个运行时的类,不能直接new,而是将运行时的类赋值给它。
获取Class实例的方式
(加载到内存中的运行时的类,会缓存一段时间,在此时间之内,可通过不同的方式来获取此运行时的类)
方法一:调用运行时类的class属性,如,Person.class
方法二:通过运行时类的对象来获取,要先创建一个实例,再通过:实例.getClass()来获取
方法三:调用Class的静态方法forName(String classPath)来获取,方法参数是类的全类名。
方法四:先获取类的加载器,再通过类加载器的loadClass方法通过全类名来获取。
@Test
public void test3() throws ClassNotFoundException {
//方式一:调用运行时类的class属性
Class clazz1=Person.class;
//输出的就是类本身
System.out.println(clazz1);
//方式二:通过运行时的类的对象
Person person=new Person();
Class clazz2=person.getClass();
System.out.println(clazz2);
//方式三:调用Class的静态方法forName(String classPath)
Class clazz3=Class.forName("org.example.polo.Person");//要写全类名
System.out.println(clazz3);
//方式四:使用类加载器ClassLoader
ClassLoader classLoader=Demo.class.getClassLoader();
Class clazz4=classLoader.loadClass("org.example.polo.Person");
System.out.println(clazz4);
//判断是否指向同一类
System.out.println("clazz1==clazz2的结果为:"+(clazz1==clazz2));
System.out.println("clazz2==clazz3的结果为:"+(clazz2==clazz3));
System.out.println("clazz3==clazz4的结果为:"+(clazz3==clazz4));
}
输出:
方法一class org.example.polo.Person
方法二class org.example.polo.Person
方法三class org.example.polo.Person
方法四class org.example.polo.Person
clazz1==clazz2的结果为:true
clazz2==clazz3的结果为:true
clazz3==clazz4的结果为:true
进程已结束,退出代码0
事实上,以下类型均可有Class对象:
class:外部类、成员(成员内部类、静态内部类)、局部内部类、匿名内部类。
interface:接口。
[]:数组,且,只要数组的元素类型和维度一样,就是同一个Class
enum:枚举
annotation:注解
primitive type:基本数据类型
void
类加载器
类加载器作用是将类(class)装载进内存,JVM规范定义了如下类型的类的加载器:
分类:
引导类加载器:用C++编写,是JVM自带的类加载器,负责Java平台核心库,用来装载核心类库,该加载器无法直接获取。
扩展类加载器:负责jre/lib/ext目录下的jar包装入工作库。
系统类加载器:负责java-classpath所指定目录下的类与jar包装入工作,是最常用的加载器。
@Test
public void test1(){
//对于自定义类,获取系统类加载器
ClassLoader classLoader=Demo.class.getClassLoader();
System.out.println(classLoader);
//获取扩展类加载器
ClassLoader classLoader1=classLoader.getParent();
System.out.println(classLoader1);
//获取引导类加载器,主要负责java核心类库的加载(如,String),不能直接获取,故为null
ClassLoader classLoader2=classLoader1.getParent();
System.out.println(classLoader2);
ClassLoader classLoader3=String.class.getClassLoader();
System.out.println("String的加载器获取结果为:"+classLoader3);
}
输出:
jdk.internal.loader.ClassLoaders$AppClassLoader@78308db1
jdk.internal.loader.ClassLoaders$PlatformClassLoader@7dc36524
null
String的加载器获取结果为:null
进程已结束,退出代码0
使用类加载器加载配置文件
@Test
public void test2() throws Exception {
Properties properties=new Properties();
/*
* 法一:使用new FileInputStream()方法来获取,此时的文件默认在当前的module下
* */
FileInputStream fileInputStream=new FileInputStream("src/main/resources/jdbc.properties");
/*
* 法二:使用ClassLoader,此时配置文件的默认识别是当前module的src下
* ClassLoader classloader=ClassLoadTest.class.getClassLoader();
* InputStream is=classloader.getResourceAsStream("jdbc.properties");
* */
properties.load(fileInputStream);
System.out.println("user="+properties.getProperty("user")+",password="+properties.getProperty("password"));
}
反射的基本使用
在使用反射前要先获取对应运行时对象的字节码文件对象.class,获取方式已在"反射的本质-获取Class实例的方式"中给出。
前置类、注解与接口
(之后测试类均是在以下类、注解、接口上进行测试)
父类Creature.java
package org.example.polo;
import java.io.Serial;
import java.io.Serializable;
public class Creature<T> implements Serializable {
@Serial
private static final long serialVersionUID = 7090496041037756443L;
private char gender;
public double weight;
private void breath(){
System.out.println("生物呼吸");
}
public void eat(){
System.out.println("生物吃东西");
}
}
注解MyAnnotation.java
package org.example.polo;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE,ElementType.FIELD,ElementType.METHOD,ElementType.CONSTRUCTOR,ElementType.LOCAL_VARIABLE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
String value()default "Hello";
}
接口MyInterface.java
package org.example.polo;
public interface MyInterface {
void info();
}
子类Person.java
package org.example.polo;
@MyAnnotation(value = "Hi")
public class Person extends Creature<String> implements Comparable<String>,MyInterface {
private String name;
int age;
@MyAnnotation(value = "abc")
public int id;
public Person() {
}
@MyAnnotation(value = "abc")
private Person(String name){
this.name=name;
}
Person(String name,int age){
this.name=name;
this.age=age;
}
@MyAnnotation
private void show(String nation){
System.out.println("I'm come from "+nation);
}
@Override
public void info() {
System.out.println("I'm a Person");
}
public String display(String interests){
return interests;
}
@Override
public int compareTo(String o) {
return 0;
}
}
获取类的对象和类的有关信息
Class类方法
public T newInstance():创建对象,内部调用了运行时类的空参构造。
使用的前提条件:运行时类必须提供空参构造,且为public(事实上,在JavaBean中要求提供一个public的空参构造器,主要为了便于通过反射创建运行时类对象,且,子类继承此运行时类时,默认调用super(),保证父类有此构造器)。
public String getName():返回全类名
public String getSimpleName():返回类名
@Test
public void test1() throws InstantiationException, IllegalAccessException {
Class clazz= Person.class;
//创建一个类的实例
Person person= (Person) clazz.newInstance();
System.out.println("创建了一个Person实例"+person);
//获取全类名
System.out.println("全类名为:"+clazz.getName());
//获取类名
System.out.println("类名为:"+clazz.getSimpleName());
}
运行结果:
创建了一个Person实例org.example.polo.Person@5a39699c
全类名为:org.example.polo.Person
类名为:Person
进程已结束,退出代码0
获取运行时类的属性结构
Class类方法:
public Field[] getFields():返回类中public修饰的属性。
public Field[] getDeclaredFields():返回类中所有属性
public Field getDeclaredField(String name):根据属性名name来获取指定属性。
Field类方法:
public String getName():返回属性名。
public int getModifiers():获取属性的修饰符,返回的修饰符是一个数字,是修饰符的代号(同样可用于类、方法)。代号如下(默认default为0,此时使用toString会返回空字符串):
PUBLIC:1
PRIVATE:2
PROTECTED:4
STATIC:8
FINAL:16
SYNCHRONIZED:32
VOLATILE:64
TRANSIENT:128
NATIVE:256
INTERFACE:512
ABSTRACT:1024
STRICT:2048
java.lang.reflect.Modifier类提供了一些静态方法:
public static String toString(int mod):输出整数对应的修饰符
public static boolean isPublic/isPrivate/isStatic(int mod):判断整数对应的是不是public
public Class<?> getType():返回属性数据类型的Class对象,可结合getName()使用来获取真正的类型。
public void set(Object obj,Object value):设置属性值。
public Object get(Object obj):读取属性的值。
public <T extends Annotation> T getAnnotation(Class<T> annotationClass):指定一个注解类型,若属性含有该注解,则返回这个注解的对象,可通过对象来获取注解的属性值。
@Test
public void test2(){
Class clazz= Person.class;
System.out.println("--------getDeclaredFields()获取类中所有属性-------");
Field[] fields=clazz.getDeclaredFields();
for (Field field:fields) {
System.out.println("属性名为:"+field.getName());
System.out.println("属性的修饰符字段为:"+field.getModifiers());
System.out.println("属性修饰符字段所对应的修饰符为:"+Modifier.toString(field.getModifiers()));
System.out.println("属性的数据类型为:"+field.getType().getName());
if(field.getAnnotation(MyAnnotation.class)!=null){
System.out.println("本属性含有MyAnnotation注解,其中,注解的value属性值为:"+field.getAnnotation(MyAnnotation.class).value());
}else {
System.out.println("本属性不含有MyAnnotation注解");
}
}
}
运行结果:
--------getDeclaredFields()获取类中public属性-------
属性名为:name
属性的修饰符字段为:2
属性修饰符字段所对应的修饰符为:private
属性的数据类型为:java.lang.String
本属性不含有MyAnnotation注解
属性名为:age
属性的修饰符字段为:0
属性修饰符字段所对应的修饰符为:
属性的数据类型为:int
本属性不含有MyAnnotation注解
属性名为:id
属性的修饰符字段为:1
属性修饰符字段所对应的修饰符为:public
属性的数据类型为:int
本属性含有MyAnnotation注解,其中,注解的value属性值为:abc
进程已结束,退出代码0
获取运行时类的方法结构
Class类方法:
public Method[] getMethods():返回类中所有方法。
public Method[] getDeclaredMethods():返回类中所有实例方法。
public Method[] getDeclaredMethod(String name,Class<?>... parameterTypes):根据方法名和方法形参获取指定方法。
Method类方法:
public String getName():返回方法名。
public int getModifiers():返回方法的修饰符代号。
public Class<?> getReturnType():返回方法返回值类型的Class对象(一般配合Class类的getSimpleName()方法使用)。
public Class<?>[] getParameterTypes():返回方法参数的修饰符Class对象数组(一般配合Class类的getSimpleName()方法使用)。
public <T extends Annotation> T getAnnotation(Class<T> annotationClass):指定一个注解类型,若方法含有该注解,则返回这个注解的对象,可通过对象来获取注解的属性值。
@Test
public void test3(){
Class clazz=Person.class;
System.out.println("----------使用getMethods()获取类中的所有方法---------");
Method[] methods=clazz.getDeclaredMethods();
for(Method method:methods){
System.out.println("方法名为:"+method.getName());
System.out.println("方法的修饰符字段为:"+method.getModifiers());
System.out.println("方法修饰符字段所对应的修饰符为:"+Modifier.toString(method.getModifiers()));
System.out.println("方法的返回值类型为:"+method.getReturnType().getSimpleName());
Class[] classesOfParameters=method.getParameterTypes();
System.out.printf("方法的参数列表为:");
for (Class c:classesOfParameters){
System.out.printf(c.getSimpleName());
}
System.out.println();
if(method.getAnnotation(MyAnnotation.class)!=null){
System.out.println("本方法含有MyAnnotation注解,其中,注解的value属性值为:"+method.getAnnotation(MyAnnotation.class).value());
}else {
System.out.println("本属性不含有MyAnnotation注解");
}
System.out.println();
}
}
运行结果:
----------使用getMethods()获取类中的所有方法---------
方法名为:compareTo
方法的修饰符字段为:1
方法修饰符字段所对应的修饰符为:public
方法的返回值类型为:int
方法的参数列表为:String
本属性不含有MyAnnotation注解
方法名为:compareTo
方法的修饰符字段为:4161
方法修饰符字段所对应的修饰符为:public volatile
方法的返回值类型为:int
方法的参数列表为:Object
本属性不含有MyAnnotation注解
方法名为:info
方法的修饰符字段为:1
方法修饰符字段所对应的修饰符为:public
方法的返回值类型为:void
方法的参数列表为:
本属性不含有MyAnnotation注解
方法名为:display
方法的修饰符字段为:1
方法修饰符字段所对应的修饰符为:public
方法的返回值类型为:String
方法的参数列表为:String
本属性不含有MyAnnotation注解
方法名为:show
方法的修饰符字段为:2
方法修饰符字段所对应的修饰符为:private
方法的返回值类型为:void
方法的参数列表为:String
本方法含有MyAnnotation注解,其中,注解的value属性值为:Hello
进程已结束,退出代码0
获取运行时类的结构
Class类方法:
public Constructor<?>[] getConstructors():获取运行时类的public构造器数组,存放的是Constructor类对象。
public Constructor<?>[] getDeclaredConstructors():获取运行时类的所有构造器数组,存放的是Constructor类对象。
public native Class<? super T> getSuperclass():获取运行时类所继承的父类,得到的是父类的Class对象。
public Class<?>[] getInterfaces():获取运行时类实现的接口的Class对象数组。
public Package getPackage():获取运行时类所在包对象(Package类型)。
public <A extends Annotation> A getAnnotations(Class<A> annotationClass):指定一个注解类型,若类含有该注解,则返回这个注解的对象,可通过对象来获取注解的属性值。
@Test
public void test4(){
Class<Person> clazz=Person.class;
System.out.println("--------------获取当前所有构造器----------");
Constructor[] constructors=clazz.getDeclaredConstructors();
for (Constructor constructor:constructors){
System.out.println(constructor);
}
System.out.print("当前类所继承的父类为:");
Class<? super Person> classOfSuperclass=clazz.getSuperclass();
System.out.println(classOfSuperclass.getSimpleName());
System.out.println("运行时类所在包为:"+clazz.getPackage().getName());
if(clazz.getAnnotation(MyAnnotation.class)!=null){
System.out.println("本类含有MyAnnotation注解,其中,注解的value属性值为:"+clazz.getAnnotation(MyAnnotation.class).value());
}else {
System.out.println("本类不含有MyAnnotation注解");
}
Class[] classesOfInterfaces=clazz.getInterfaces();
System.out.println("当前类所实现的接口有:");
for(Class c:classesOfInterfaces){
System.out.println(c.getName());
}
}
运行结果:
--------------获取当前所有构造器----------
org.example.polo.Person(java.lang.String,int)
private org.example.polo.Person(java.lang.String)
public org.example.polo.Person()
当前类所继承的父类为:Creature
运行时类所在包为:org.example.polo
本类含有MyAnnotation注解,其中,注解的value属性值为:Hi
当前类所实现的接口有:
java.lang.Comparable
org.example.polo.MyInterface
进程已结束,退出代码0
修改运行时类的属性
@Test
public void test3() throws InstantiationException, IllegalAccessException, NoSuchFieldException {
Class clazz= Person.class;
//创建运行时类的对象
Person person=(Person) clazz.newInstance();
//获取指定的属性
//要求运行时类中声明属性为public
Field id=clazz.getField("id");
/*
* 设置当前属性的值
* set():
* 参数1:指明设置哪个对象的属性
* 参数2:将此属性设置为多少
* */
id.set(person,23);
/*
* 获取当前属性的值
* get():
* 参数1:获取哪个属性的值
* */
int pId=(int) id.get(person);
System.out.println(pId);
}
public void test4() throws InstantiationException, IllegalAccessException, NoSuchFieldException {
Class clazz=Person.class;
//创建运行时类的对象
Person person=(Person) clazz.newInstance();
//getDeclaredField(String fieldName):
Field name=clazz.getDeclaredField("name");
//2.保证当前属性是可访问的
name.setAccessible(true);
//3.获取、设置指定对象的此属性值
name.set(person,"Tom");
System.out.println(name.get(person));
}
调用运行时类的方法
@Test
public void test5() throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Class clazz= Person.class;
//创建运行时类的对象
Person person=(Person) clazz.newInstance();
/*
* 获取指定的某个方法
* getDeclaredMethod():
* 参数1:指明获取的方法的名称
* 参数2:指明获取方法的形参列表
* */
Method show=clazz.getDeclaredMethod("show",String.class);
/*
* invoke():
* 参数1:方法的调用者
* 参数2:给方法形参赋值的实参
* 返回值:对应类中调用的方法的返回值
* */
System.out.println(show.invoke(person,"CHN"));
System.out.println("------------静态方法的调用----------------");
Method showDesc= clazz.getDeclaredMethod("showDesc");
showDesc.setAccessible(true);
//若无返回值,则为null
Object returnVal=showDesc.invoke(Person.class);
System.out.println(returnVal);
}
调用运行时类的构造器
@Test
public void test6() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Class clazz=Person.class;
//private Person(String name)
/*
* 1.获取指定的构造器
* getDeclaredConstructor():
* 参数:指明构造器的参数列表
* */
Constructor constructor=clazz.getConstructor(String.class);
//2.保证构造器是可访问的
constructor.setAccessible(true);
//3.调用构造器创建运行时类的对象
Person person=(Person) constructor.newInstance("Tom");
System.out.println(person);
}
总结
以上就是本博客的全部内容,而关于一些类的分析,如,常用的异常处理类Exception、IO流处理类等以及一些新特性语法,如,Lambda表达式的使用,我会写在本专栏的其他博客当中,避免内容过于繁杂,也方便自己后面复习、修改。