Java基础
Java基础
Java概述
Java语言的优缺点:
优点:
- 跨平台性:跨平台性:这是Java的核心优势。Java在最初设计时就很汪重移植和跨平台性。比如: Java的int水远都是32位。不像C++可能是16,32,可能是根据编译器厂商规定的变化。通过Java语言编写的应用程序在不同的系统平台上都可以运行。“Write onceRun Anywhere ”。
- 原理:只要在需要运行java应用程序的操作系统上,先安装一个Java虚拟机(JVM,Java Virtual Machine)即可。由JVM来负责Java程序在该系统中的运行。
JVM的作用就好像
在下载JDK时,我们要严格按照对应的操作系统来下载对应的JDK,因为JDK中包含的JVM不同,JVM所针对的操作系统也不同
面向对象性:面向对象是一种程序设计技术,非常适合大型软件的设计和开发。面向对象编程支持封装、继承、多态等特性,让程序更好达到高内聚,低耦合的标准。
健壮性:健壮性:吸收了c/C++语言的优点,但去掉了其影响程序健壮性的部分(如指针、内存的申请与释放等),提供了一个相对安全的内存管理和访问机制。
安全性高:Java适合于网络/分布式环境,需要提供一个安全机制以防恶意代码的攻击。如:安全防范机制(ClassLoader类加载器),可以分配不同的命名空间以防替代本地的同名类、字节代码检查。
简单性:Java就是c++语法的简化版,我们也可以将Java称之为“C++–”。比如:头文件,指针运算,结构,联合,操作符重载,虚基类等。
高性能:Java最初发展阶段,总是被人诟病“性能低”;客观上,高级语言运行效率总是低于低级语言的,这个无法避免。Java语言本身发展中通过虚拟机的优化提升了几十倍运行效率。比如,通过JIT(JUST IN TIME)即时编译技术提高运行效率。Java低性能的短腿,已经被完全解决了。业界发展上,我们也看到很多C++应用转到Java开发,很多C++程序员转型为Java程序员。
缺点:
- 语法过于复杂、严谨,对程序员的约束比较多,与python、php等相比入门较难。但是一旦学会了,就业岗位需求量大,而且薪资待遇节节攀升。
- 一般适用于大型网站开发,整个架构会比较重,对于初创公司开发和维护人员的成本比较高(即薪资高)选择用Java语言开发网站或应用系统的需要一定的经济实力。
- 并非适用于所有领域。比如,objective C、Swift在ioS设备上就有着无可取代的地位。浏览器中的处理几乎完全由JavaScript掌控。windows程序通常都用c++t或c#编写。Java在服务器端编程和跨平台客户端应用领域则很有优势。
JVM功能特点
JVM (J吧 ava V irtual Machine,Java虚拟机)︰是一个虚拟的计算机,是Java程序的运行环境。JVM具有指令集并使用不同的存储区域,负责执行指令,管理数据、内存、寄存器。
JVM功能
- 实现Java语言的跨平台性:我们编写的Java代码,都运行在JVM之上,正因为有了JVM才使得Java程序具备跨平台性
使用JVM前后的对比
自动的内存管理(内存分配、内存回收)
Java程序在运行过程中,涉及到运算的数据的分配、存储等都由JVM来完成
Java消除了程序员回收无用内存空间的职责。提供了一种系统级线程跟踪存储空间的分配情况,在内存空间达到相应阈值时,检查并释放可被释放的存储器空间。
GC的自动回收,提高了内存空间的利用效率,也提高了编程人员的效率,很大程度上减少了因为没有释放空间而导致的内存泄漏。
变量和运算符
关键字(KeyWords)
- 定义:被Java语言赋予了特殊含义,被用作专门用途的字符串(或单词)
- 例如:class,public,static,void等,这些单词已经被Java定义好了
- 特点:全部关键字都是小写字母
48个关键字:abstract、assert、boolean、break、byte、case、catch、char、class、continue、default、do、double、else、enum、extends、final、finally、float、for、if、implements、import、int、interface、instanceof、long、native、new、package、private、protected、public、return、short、static、strictfp、super、switch、synchronized、this、throw、throws、transient、try、void、volatile、while。
2)2个保留字(现在没用以后可能用到作为关键字):goto、const。
3)3个特殊直接量:true、false、null。
标识符(identifier)
Java中变量、方法、类等要素命名时所使用的字符序列,称为标识符
标识符的命名规则
- 由26个英文字母大小写,8-9 ,_或$组成数字不可以开头。
- 不可以使用关键字和保留字,但能包含关键字和保留字。Java中严格区分大小写,长度无限制。
- 标识符不能包含空格。
例如:类名,对象名,变量名等自己所命名的
变量
一花一世界,如果把一个程序看做一个世界或一个社会的话,那么变量就是程序世界的花花草草、万事万物。即
变量是程序中不可或缺的组成单位,最基本的存储单元。
初识变量
变量的概念:
内存中的一个存储区域,该区域的数据可以在同一类型范围内不断变化
变量的构成包含三个要素,数据类型、变量名、存储值
Java中变量声明的格式 : 数据类型 变量名 = 变量值
变量的作用:用于在内存中存储数据
使用变量注意:
- Java中每个变量必须先声明后使用
- 使用变量名来访问这片区域的数据
Java中的变量按照数据类型分类:
基本数据类型:
- 整型:byte、short、int、long
- 浮点型:float、double
- 字符型:char
- 布尔类型:boolen
引用数据类型:
- 类(class)
- 数组(array)
- 接口(interface)
- 枚举(enum)
- 注解(annotation)
- 记录(record)
整型
Java各整数类型ou固定的表数范围和字段长度,不收具体操作系统的影响,以保证Java程序的可移植性
浮点型
与整型类型相似,Java浮点型也有固定的标数范围和字段长度,不收具体操作系统影响
浮点型常量有两种表现形式:
十进制数形式:5.12 512.0f .512(必须有小数点)
科学计数法形式。入:5·.12e2 512E2 100E-2
float:单精度,位数可以精确到7位有效数字,很多情况下很难满足需求
double:双精度,京都市float的两倍,通常采用此类型。
定义float类型的变量,赋值时需要以 f 或 F作为后缀
Java的浮点型 常量默认为double型
关于浮点型精度的说明
并不是所有的小数都能可以精确的用二进制浮点数表示。二进制浮点数不能精确的表示0.1、0.01、0.001这样10的负次幂。
浮点类型float、double的数据不适合在不容许舍入误差的金融计算领域。如果需要精确数字计算或保留指定位数的精度,需要使用BigDecimal类。
字符型
char型数据用来表示通常意义上“字符”(占2字节)
Java中的所有字符都使用Unicode编码,故一个字符可以存储一个字母,一个汉字,或其他书面语的一个字符。
字符型变量的三种表现形式:
。形式1:使用单引号(‘’)括起来的单个字符。例如: char c1 = ‘a’; char c2= ‘中’; char c3 = ‘9’;
。形式2:直接使用Unicode值来表示字符型常量:‘\uXXXX’。其中,XXxx代表一个十六进制整数。例如: \u0023表示 ‘#’。
。形式3:Java中还允许使用转义字符’'来将其后的字符转变为特殊字符型常量。例如: char c3 = ‘\n’;//‘\n’表示换行符
字符串类型(String)
String类型概述
- String类,属于引用数据类型,俗称字符串。
- String类型的变量,可以使用一对””的方式进行赋值。
- String声明的字符串内部,可以包含0个,1个或多个字符。
String与基本数据类型变量间的运算
- 这里的基本数据类型包括boolean在内的8种。
- String与基本数据类型变量间只能做连接运算,使用”+”表示。,运算结果只能是String类型
循环结构
break和continue的区别是?
break会跳出当前循环结构,continue会跳出当次循环结构
数组
概述
数组(Array),是多个相同类型数据按一定顺序排列的集合,并使用一个名字命名,并通过编号的方式对这些数据进行统一管理。
数组四要素
数组名
下标(或索引)
元素
数组的长度
数组的特点
- 数组本身是引用数据类型,而数组中的元素可以是任何数据类型,包括基本数据类型和引用数据类型。
- 创建数组对象会在内存中开辟一整块连续的空间。占据的空间的大小,取决于数组的长度和数组中元素的类型。
- 数组中的元素在内存中是依次紧密排列的,有序的。
- 数组,一旦初始化完成,其长度就是确定的。数组的长度一旦确定,就不能修改。我们可以直接通过下标(或索引)的方式调用指定位置的元素,速度很快。
- 数组名中引用的是这块连续空间的首地址。
数组初始化
数组有两种初始化方式:静态初始化,动态初始化
静态初始化:创建时定义数组中的内容,长度为数组中元素的个数
1 | //静态初始化 |
动态初始化:创建时定义数组的最大长度,内容不确定
1 | //动态初始化 |
数组 初始化默认值
整型(int long byte)的 数组初始化值为 0
浮点型(double float)的数组初始化值为0.0
字符型(char) 的数组初始化值为’0’
布尔型(boolen)的数组初始化值为 false
一维数组的内存解析
java虚拟机的划分
为了提高运算效率,就对空融进行了不同区域的划分,因为每一片区域都有特定的处理数据方式和内存管理方式。
数组在内存中的划分是这样的
数组工具类Arrays
java.util.Arrays类即为操作数组的工具类,包含了用来操作数组(比如排序和搜索)的各种方法。
1 | Arrays.equals(a[n],b[n]) //比较两个数组内容是否相同 |
面向对象
面向对象的基本概述
- .Java类及类的成员:属性、方法、构造器,代码块、内部类
- 面向对象的特征:封装、继承、多态、(抽象)
- 其他关键字的使用: this、super、package、import、static、final、interface、abstract
属性与局部变量的不同点
类中声明的位置的不同:
属性:声明在类内,方法外的变量
局部变量:声明方法、构造器内部的变量
在内存中分配的位置不同:
- 属性:随着对象的创建,存储在堆空间中
- 局部变量:存储在栈空间中
生命周期:
- 属性:随着对象的创建而创建,随着对象的消亡而消亡。
- 局部变量:随着方法对应的栈帧入栈,局部变量会在栈中分配﹔随着方法对应的栈帧出栈,局部变量消亡。
作用域:
- 属性:在整个类的内部都是有效的
- 局部变量:仅限于声明此局部变量所在的方法(或构造器、代码块内)
是否可以有权限修饰符进行修饰:
都有哪些权限修饰符: public、protected、缺省、private。
属性,是可以使用权限修饰符进行修饰的。
而局部变量,不能使用任何权限修饰符进行修饰的。
是否有默认值:(重点)属性:都有默认初始化值
意味着,如果没有给属性进行显式初始化赋值,则会有默认初始化值。
局部变量:都没有默认初始化值。
意味着,在使用局部变量之前,必须要显式的赋值,否则报错。
静态变量
静态变量:类中的属性使用static进行修饰。
对比静态变量与实例变量:
①:个数
静态变量:在内存空间中只有一份,被类的多个对象所共享
实例变量:类的每一个实例(或对象)都保存着一份实例变量。
②:内存位置
静态变量:jdk6及之前:存放在方法区。jdk7及之后:存放在堆空间
实例变量:存放在堆空间
③:加载时机
静态变量,随着类的加载而加载由于类只会加载一次,所以静态变量只有一份
实例变量:随着对象的创建而加载,每个对象都拥有一份实例变量
④:调用者
静态变量:可以被类直接调用,也可以适用对象调用
实例变量:只能使用对象进行调用
⑤:判断是否可以调用—–>从生命周期的角度解释
类: 类变量:yes 实例变量 :no
对象: 类变量 yes 实例变量:yes
代码块
静态代码块:在类加载时执行
代码块:在对象创建时执行
包装类
1.为什么要使用包装类?
为了使得基本数据类型的变量具备引用数据类型变量的相关特征(比如:封装性、继承性、多态性),我们给各个基本数据类型的变量都提供了对应的包装类。
2.为什么需要包装类和基本数据类型的转换
一方面,在有些场景下,需要使用基本数据类型对应的包装类的对象。此时就需要将基本数据类型的变量转换为包装类的对象。比如: ArrayList的add(object obj);0bject类的equals(Object obj)
对于包装类来讲,既然我们使用的是对象,那么对象是不能进行+– */等运算的。
异常处理
异常类或接口的层次结构图
Error: Java虚拟机无法解决的严重问题。如:JVM系统内部错误、资源耗尽等严重情况。一般不编写针对性的代码进行处理。例如: StackOverflowError (栈内存溢出)和outOfMemoryError (堆内存溢出,简称oOM)。
Exception:其它因编程错误或偶然的外在因素导致的一般性问题,需要使用针对性的代码进行处理,使程序继续运行。否则一旦发生异常,程序也会挂掉。例如:
- 空指针访问
- 试图读取不存在的文件。网络连接中断
- 数组角标越界
编译时异常与运行时异常
Java程序的执行分为编译时过程和运行时过程。有的错误只有在运行时才会发生。比如:除数为0,数组下标越界等。
编译时期异常(即checked异常、受检异常)︰在代码编译阶段,编译器就能明确警示当前代码可能发生(不是一定发生)xx异常,并明确督促程序员提前编写处理它的代码。如果程序员没有编写对应的异常处理代码,则编译器就会直接判定编译失败,从而不能生成字节码文件。通常,这类异常的发生不是由程序员的代码引起的,或者不是靠加简单判断就可以避免的,例如: FileNotFoundException(文件找不到异常)。
运行时期异常(即runtime异常、uncheck异常、非受检异常)︰在代码编译阶段,编译器完全不做任何检查,无论该异常是否会发生,编译器都不给出任何提示。只有等代码运行起来并确实发生了xx异常,它才能被发现。通常,这类异常是由程序员的代码编写不当引起的,只要稍加判断,或者细心检查就可以避免。
final、finally、finalize的区别
1、final修饰符(关键字)。被final修饰的类,就意味着不能再派生出新的子类,不能作为父类而被子类继承。因此一个类不能既被abstract声明,又被final声明。将变量或方法声明为final,可以保证他们在使用的过程中不被修改。被声明为final的变量必须在声明时给出变量的初始值,而在以后的引用中只能读取。被final声明的方法也同样只能使用,即不能方法重写。
2、finally是在异常处理时提供finally块来执行任何清除操作。不管有没有异常被抛出、捕获,finally块都会被执行。try块中的内容是在无异常时执行到结束。catch块中的内容,是在try块内容发生catch所声明的异常时,跳转到catch块中执行。finally块则是无论异常是否发生,都会执行finally块的内容,所以在代码逻辑中有需要无论发生什么都必须执行的代码,就可以放在finally块中。
3、finalize是方法名。java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在object类中定义的,因此所有的类都继承了它。子类覆盖finalize()方法以整理系统资源或者被执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。
多线程
程序、进程、线程
程序(program):为完成特定任务,用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
进程(process)︰程序的一次执行过程,或是正在内存中运行的应用程序。如:运行中的oQ,运行中的网易音乐播放器。
每个进程都有一个独立的内存空间,系统运行一个程序即是一个进程从创慧运行到消亡的过程。(生命周期)
程序是静态的,进程是动态的
进程作为操作系统调度和分配资源的最小单位((亦是系统运行程序的基本单位),系统在运行时会为每个进程分配不同的内存区域。
现代的操作系统,大都是支持多进程的,支持同时运行多个程序。比如:现在我们上课一边使用编辑器,—边使用录屏软件,同时还开着画图板,dos窗口等软件。
线程(thread)︰进程可进一步细化为线程,是程序内部的一条执行路径。一个进程中至少有一个线程。。一个进程同一时间若并行执行多个线程,就是支持多线程的。
- 线程作为`CPU调度和执行的最小单位
一个进程中的多个线程共享相同的内存单元,它们从同一个堆中分配对象,可以访问相同的变量和对象。这就使得线程间通信更简便、高效。但多个线程操作共享的系统资源可能就会带来安全的隐患。
下图中,红框的蓝色区域为线程独享,黄色区域为线程共享。
- 线程作为`CPU调度和执行的最小单位
不同的进程之间是不共享内存的。
进程之间的数据交换和通信的成本很高。
线程调度
分时调度
所有线程轮流使用CPU的使用权,并且平均分配每个线程占用CPu的时间。
抢占式调度
让优先级高的线程以较大的概率优先使用CPU。如果线程的优先级相同,哪么会随机选择一个(线程随机性).Java使用的为抢占式调度。
多线程程序的优点
背景:以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短,为何仍需多线程呢?
多线程程序的优点:
1.提高应用程序的响应。对图形化界面更有意义,可增强用户体验。
2.提高计算机系统CPU的利用率
3.改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改
并行与并发
并行(parallel):指两个或多个事件在同一时刻发生(同时发生)。指在同一时刻,有多条指令在多个CPU 上同时执行。比如:多个人同时做不同的事。
并发(concurrency)︰指两个或多个事件在同一个时间段内发生。即在一段时间内,有多条指令在单个cPU上快速轮换、交替执行,使得在宏观上具有多个进程同时执行的效果。
创建启动线程
- Java语言的JVM允许程序运行多个线程,使用
java.lang. Thread
类代表线程所有的线程对象都必须是Thread类或其子类的实例。 - Thread类的特性
- 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,因此把run()方法体称为线程执行体。。通过该Thread对象的start()方法来启动这个线程,而非直接调用run()
- 要想实现多线程,必须在主线程中创建新的线程对象。
创建线程的方式
①:继承Thread类
- 创建Thread类的子类
- 重写run()方法—–>将此线程要执行的操作生命在方法体
- 创建当前Thread子类的对象
- 通过对象调用start()方法
1 | public class MyThread extends Thread{ |
1 | public class main { |
执行结果如下
注意:一个线程对象不能start多次,可以 创建一个类的多个线程对象去调用start方法
②实现Runnable接口
- 创建一个类实现Runnable接口
- 实现run()方法
- 创建 实现类
- 创建Thread对象并在创建时传入参数(实现类)
- 调用Thread对象start方法
1 | public class yourThread implements Runnable{ |
1 |
|
对比两种方式
共同点:启动线程,使用的都是Thread类中定义的start()创建的线程对象,都是Thread类或其子类的实例。
不同点:一个是类的继承,一个是接口的实现。
建议。建议使用实现Runnable接口的方式。
Runnable方式的好处:
①实现的方式,避免的类的单继承的局限性
②更适合处理有共享数据的问题。
③实现了代码和数据的分离。
线程中的常用方法
start():1启动线程②调用线程的run()
run()∶将线程要执行的操作,声明在run()中。
currentThread():获取当前执行代码对应的线程
getName():获取线程名
setName():设置线程名
sleep(long millis):静态方法,调用时,可以使得当前线程睡眠指定的毫秒数
yield():静态方法,一旦执行此方法,就释放CPU的执行权
join():在线程a中通过线程b调用join(),意味着线程a进入阻塞状态,直到线程b执行结束,线程a才结束阻塞状态,继续执行方法
isAlive()∶判断当前线程是否存活
线程的优先级:
getPriority():获取线程的优先级
setPriority()∶设置线程的优先级。范围[1,10]
Thread类内部声明的三个常量:
MAX_PRIORITY ( 10):最高优先级
MIN _PRIORITY (1)﹔最低优先级
NORM_PRIORITY (5):普通优先级,默认情况下main线程具有普通优先级。
线程的生命周期
这不我操作系统中的进程的生命周期嘛!!!!!
线程同步
Java是如何解决线程的安全问题的?
使用线程的同步机制。
方式1:同步代码块
1 | synchronized(同步监视器){ |
1 | public class MyThread implements Runnable{ |
1 | public class main { |
说明:
需要被同步的代码,即为操作共享数据的代码。
共享数据:即多个线程都需要操作的数据。比如: ticket
需要被同步的代码,在被synchronized包裹以后,就使得一个线程在操作这些代码的过程中,其它线程必须等待>同步监视器,俗称锁。哪个线程获取了锁,哪个线程就能执行需要被同步的代码。
同步监视器,可以使用任何一个类的对象充当。但是,多个线程必须共用同一个同步监视器。
注意:在实现Runnable接口的方式中,同步监视器可以考虑使用: this。在继承Thread类的方式中,同步监视器慎用当前类 .class
方式2:同步方法
说明:
如果操作共享数据的代码完整的声明在了一个方法中,那么我们就可以将此方法声明为同步方法即可。非静态的同步方法,默认同步监视器是this
静态的同步方法,默认同步监视器是当前类本身。
1 | public synchronized void run() {} |
单例模式中的懒汉模式的线程安全问题
懒汉单例模式是容易出现线程安全问题的,容易出现单例对象并不 “单例”
1 | public class lazyPerson implements Runnable { |
主方法类
1 | public class main { |
输出结果:
我们发现单例模式创建的对象并不唯一,所以出现了线程安全问题。如何解决?
- 使用synchronized()方法包裹代码块,实现线程同步
- 或使用synchronized关键字修饰方法
死锁
死锁是一种非常严重的bug,是说多个线程同时被阻塞,线程中的一个或者多个又或者全部都在等待某个资源被释放,造成线程无限期的阻塞,导致程序不能正常终止
诱发死锁的原因:·
互斥条件
占用且等待
不可抢夺(或不可抢占)
循环等待
以上4个条件,同时出现就会触发死锁。
解决死锁:
死锁一旦出现,基本很难人为干预,只能尽量规避。可以考虑打破上面的诱发条件。
针对条件1:互斥条件基本上无法被破坏。因为线程需要通过互斥解决安全问题。
针对条件2:可以考虑一次性申请所有所需的资源,这样就不存在等待的问题。
针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就主动释放掉已经占用的资源。
针对条件4∶可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。
ReentraLock的基本使用
ReentraLock是java.util.concurrent(JUC)包下的一个包中的类,实现了Lock接口多用于线程安全控制
1.步骤:
步骤
1.创建Lock的实例,需要确保多个线程共用同一个Lock实例!需要考虑将此对象声明为static final
2.执行lock()方法,锁定对共享资源的调用
- unlock()的调用,释放对共享数据的锁定
1 | package java基础; |
synchronized同步的方式与Lock的对比?
synchronized不管是同步代码块还是同步方法,都需要在结束一对f之后,释放对同步监视器的调用。
Lock是通过两个方法控制需要被同步的代码,更灵活一些。
Lock作为接口,提供了多种实现类,适合更多更复杂的场景,效率更高。
线程通信
1.线程间通信的理解
当我们需要多个线程
来共同完成一件任务,并且我们希望他们有规律的执行,那么多线程之间需要一些通信机制,可以协调它们的工作,以此实现多线程共同操作一份数据。
2.涉及到三个方法的使用:
wait():线程一旦执行此方法,就进入等待状态。同时,会释放对同步监视器的调用
notify():一旦执行此方法,就会唤醒被wait()的线程中
notifyAll(:一旦执行此方法,就会唤醒所有被wait的线程。
sleep() 和 wait() 有什么区别?
- 类的不同:sleep() 来自 Thread,wait() 来自 Object。
- 释放锁:sleep() 不释放锁;wait() 释放锁。
- 用法不同:sleep() 时间到会自动恢复;wait() 可以使用 notify()/notifyAll()直接唤醒。
练习:一起买票
1 | public class MyThread implements Runnable{ |
1 | public static void main(String[] args) { |
其他创建线程的方式
实现Callable接口
1 | public class MyCallThread implements Callable { |
用法和Runnable类似,不过貌似嵌套了一层FutureTask,用法如下
1 | public static void main(String[] args) throws ExecutionException, InterruptedException { |
使用线程池创建线程
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
那么有没有一种办法使得线程可以复用,即执行完一个任务,并不被销毁,而是可以继续执行其他的任务?
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
使用线程池的优点:
提高了程序执行的效率。(因为线程已经提前创建好了)
提高了资源的复用率。(因为执行完的线程并未销毁,而是可以继续执行其他的任务)
可以设置相关的参数,对线程池中的线程的使用进行管理
1 | ExecutorService service = Executors.newFixedThreadPool(10); |
String
概述
细看一下String类
1 | public final class String |
final:String是不可被继承的
Serializable:可序列化的接口。凡是实现此接口的类的对象就可以通过网络或本地流进行数据的传输。
comparable :凡是实现此接口的类,其对象都可以比较大小。
jdk8:
1 | //该值用于字符存储 |
jdk9后:
1 | /*该值用于字符存储 |
字符串常量的存储位置
字符串常量都存储在字符串常量池(StringTable)中字符串常量池不允许存放两个相同的字符串常量。
在JDK7之前,字符串常量池从存放在方法区中,在JDK7之后,存放在堆空间中
字符串在进行修改的过程中实际上是在堆空间新创建了一个字符串对象,赋值为修改后的值
StringAPI
replace(char oldchar,char newchar) :创建String对象并赋值为replace替换后的值,并不改变原有的常量池中的值, 例如
1 | String s2="hello"; |
intern() 在常量池中创建该字符串常量,返回字符串对象,对象指向常量池中的地址
String.valueof(int short byte long) 将整型变量转化为String字符串
tocharArray() 将字符串转化为 字符数组
getBytes(); 返回字节数组,String与byte[]之间的转换(在utf-8字符集中,一个汉字占用3个字节,一个字母占用1个字节.在gbk字符集中,一个汉字占用2个字节,一个字母占用1个字节)
编码与解码:l
编码: String —>字节或字节数组
解码:字节或字节数组—-> String
要求:解码时使用的字符集必须与编码时使用的字符集一致,不一致,就会乱码
(1 ) boolean isEmpty():字符串是否为空
(2 ) int length():返回字符串的长度
(3 ) String concat(xx):拼接
(4) boolean equals(0bject obj):比较字符串是否相等,区分大小写
(5) boolean equalsIgnoreCase(object obj):比较字符串是否相等,不区分大小写
(6)int compareTo(String other):比较字符串大小,区分大小写,按照Unicode编码值比较大小
(7) int compareToIgnoreCase(String other):比较字符串大小,不区分大小写
(8 String toLowerCase():将字符串中大写字母转为小写
(9 ) String touppercase():将字符串中小写字母转为大写
(10) String trim ():去掉字符串前后空白符
(11) public String intern():结果在常量池中共享
(11 ) boolean contains(xx):是否包含xx
(12) int index0f(xx):从前往后找当前字符串中xx,即如果有返回第一次出现的下标,要是没有返回-1
(13)int indexOf(String str, int fromIndex):返回指定子字符串在此字符串中第一次出现处的索引
(14)int lastIndex0f(xx):从后往前找当前字符串中xx,即如果有返回最后一次出现的下标,要是没有返回
(15)int lastIndex0f(String str, int fromIndex):返回指定子字符串在此字符串中最后一次出现处
(18 ) char charAt( index):返回[index]位置的字符
(19) char[ ] toCharArray():将此字符串转换为一个新的字符数组返回
(20 ) static String value0f(char[] data) :返回指定数组中表示该字符序列的String
(21) static String valve0f(char[] data,int offset, int count):返回指定数组中表示该字符序
(22) static String copyValue0f(char[] data):返回指定数组中表示该字符序列的String
(23) static String copyValue0f(char[] data,int offset,int count):指定字符数组中的所有字符复制到一个新的字符数组中,并返回一个新的字符串
(24) boolean startsWith(xx):测试此字符串是否以指定的前缀开始
(25) boolean startsWith(String prefix, int toffset):测试此字符串从指定索引开始的子字符串是否以指定的前缀开始
(26) boolean endsWith(xx):测试此字符串是否以指定的后缀结束
replace(char oldChar,char newChar):
replace(CharSequence target,CharSequence replacement)
replaceAll(String regex,String replacement)
replaceFirst(String regex,String replacement):
StringBuffer 、StringBuilder
String:不可变的字符序列
StringBuffer:可变的字符序列;JDK1.0声明,线程安全的,效率低
StringBuilder:可变的字符序列;JDK5.0声明,线程不安全的,效率高
常用API
增:append(xx)
删:delete(int start, int end) deleteCharAt(int index)
改: replace(int start, int end,String str) setCharAt(int index,char c)
查: charAt(int index)
插:insert(int index, xx)
长度: length()
效率从高到低排列:
StringBuilder > StringBuffer > String
1.实现对象的排序,可以考虑两种方法:自然排序、定制排序
2.方式一:实现Comparable接口的方式
实现步骤:
具体的类A实现Comparable接口,重写Comparable接口中的compareTo(Object obj)方法,在此方法中指明比较类A的对象的大小的标准3创建类A的多个实例,进行大小的比较或排序。
3.方式二:实现Comparato接口的方式
实现步骤:
创建一个实现了Comparator接口的实现类A
实现类A要求重写Comparator接口中的抽象方法compare(Object o1,0bject o2),在此方法中指明
创建此实现类的对象
集合
Collection 接口的接口 对象的集合(单列集合)
├——-List 接口:元素按进入先后有序保存,可重复
│—————-├ LinkedList 接口实现类, 链表, 插入删除, 没有同步, 线程不安全
│—————-├ ArrayList 接口实现类, 数组, 随机访问, 没有同步, 线程不安全
│—————-└ Vector 接口实现类 数组, 同步, 线程安全
│ ———————-└ Stack 是Vector类的实现类
└——-Set 接口: 仅接收一次,不可重复,并做内部排序
├—————-└HashSet 使用hash表(数组)存储元素
│————————└ LinkedHashSet 链表维护元素的插入次序
└ —————-TreeSet 底层实现为二叉树,元素排好序
Map 接口 键值对的集合 (双列集合)
├———Hashtable 接口实现类, 同步, 线程安全
├———HashMap 接口实现类 ,没有同步, 线程不安全-
│—————–├ LinkedHashMap 双向链表和哈希表实现
│—————–└ WeakHashMap
├ ——–TreeMap 红黑树对所有的key进行排序
└———IdentifyHashMap
Conllection:常用方法:
add(object)把元素添加到集合中
addAll(Collection c):把c集合中的所有元素添加到集合中
clear():清空集合
contains(Object o):判断o元素在集合是否存在
containsAll(Collection c) 是否包含集合c中的所有元素
retainAll(Collection c) 返回两个集合的交集
remove(Object o):从集合中删除元素o
remove(Collection c ) 与集合c求差集
size():返回集合的长度
toArray():将集合转化为数组
List
List接口中存储数据的特点:用于存储有序的、可以重复的数据。—>使用List替代数组,”动态”数组
—- ArrayList:List的主要实现类,线程不安全的、效率高﹔底层使用Object[]数组存储
在添加数据、查找数据时,效率较高;在插入、删除数据时,效率较低
|—- LinkedList:底层使用双向链表的方式进行存储; 在对集合中的数据进行频繁的删除、插入操作时,建议使用此类
在插入、删除数据时,效率较高;在添加数据、查找数据时,效率较低;
—- VectorList的古老实现类;线程安全的、效率低﹔底层使用0bject[]数组存储
Set
l—–子接口:Set:存储无序的、不可重复的数据(高中学习的集合)
l—- HashSet:主要实现类,底层使用了HashMap,即使用数组+单向链表+红黑树结构进行存储(JDK8)
l—- LinkedHashSet.是HashSet的子类﹔在现有的数组+单向链表+红黑树结构的基础上,又添加了一组双向链表,用于记录添加元素的先后顺序。即:我们可以按照添加元素的顺序实现遍历。便于频繁的查询操作。
|—- TreeSet:底层使用红黑树存储。可以按照添加的元素的指定的属性的大小顺序进行遍历。
Map
java.util.Map:存储一对一对的数据(key-value键值对,(x1,y1)、(x2,y2) --> y=f(x),类似于高中的函数)
l—- HashNap:主要实现类;线程不安全的,效率高;可以添加null的key和value值;底层使用数组+单向链表+红黑树结构存储(jdk8)
l—- Hashtable:古老实现类;线程安全的,效率低;可以添加null的key或value值;底层使用数组+单向链表结构存储(jdk8)
l—- LinkedHashMap:是HashMap的子类;在HashMap使用的数据结构的基础上,增加了一对双向链表,用于记录添加的元素的先后顺序进而我们在遍历元素时,就可以按照添加的顺序显示。
l—- TreeMap::底层使用红黑树存储;可以按照添加的key-value中的key元素的指定的属性的大小顺序进行遍历
l—- Properties
增:
put(Object key,0bject value)putAll(Map m)
删:
0bject remove(0bject key)改:
put(Object key ,0bject value)putAll(Map m)
查:
0bject get(0bject key)
长度:
size()
遍历:
遍历key集: Set keySet()
遍历value集: Collection values ()
遍历entry集: Set entryset()
File类和IO流
public File(String pathname):以pathname为路径创建File对象,可以是绝对路径或者相对路径
public File(String parent,String child) :以parent为父路径,child为子路径创建File对象
public File(File parent,String child)﹔根据一个父File对象和子文件路径创建File对象
获取目录以及基本信息
public String getName() :获取名称
public String getPath() :获取路径
public String getAbsolutePath()`:获取绝对路径
public File getAbsoluteFile():获取绝对路径表示的文件
public String getParent():获取上层文件目录路径。若无,返回null
public long length() :获取文件长度(即:字节数)。不能获取目录的长度。
public long lastNodified() :获取最后一次的修改时间,毫秒值
列出目录的下一级
public String[ ] list() :返回一个String数组,表示该File目录中的所有子文件或目录。
public File[] listFiles()﹔返回一个File数组,表示该File目录中的所有的子文件或目录。
反射机制
背景
Java程序中,所有的对象都有两种类型:编译时类型和运行时类型,而很多时候对象的编译时类型和运行时类型不一致。Object obj = new String(“hello”);
例如:某些变量或形参的声明类型是object类型,但是程序却需要调用该对象运行时类型的方法,该方法不是object中的方法,那么如何解决呢?
解决这个问题,有两种方案:
- 方案1∶在编译和运行时都完全知道类型的具体信息,在这种情况下,我们可以直接先使用instanceof运算符进行判断,再利用强制类型转换符将其转换成运行时类型的变量即可。
- 方案2:编译时根本无法预知该对象和类的真实信息,程序只能依靠运行时信息.来发现该对象和类的真实信息,这就必须使用反射。
概述
Reflection(反射)是被视为动态语言的关键,反射机制允许程序在运行期间借助于Reflection APl取得任何类的内部信息,并能直接操作任意对象的内部属性及方法。
加载完类之后,在堆内存的方法区中就产生了一个class类型的对象(一个类只有一个Class对象),这个对象就包含了完整的类的结构信息。我们可以通过这个对象看到类的结构。这个对象就像一面镜子,透过这个镜子看到类的结构,所以,我们形象的称之为,反射。
(FileInputStream默认读取的是该moudle下的文件,ClassLoader.getSystemClassLoader().getResourceAsStream()默认读取的是该moudle下的src文件)
类的加载与ClassLoader的理解
类的生命周期
类在内存中完整的生命周期:加载–>使用–>卸载。其中加载过程又分为:装载、链接、初始化三个阶段。
类的加载过程
当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过加载、链接、初始化三个步骤来对该类进行初始化。如果没有意外,JVM将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载。
类的加载又分为三个阶段:
(1)装载(Loading)
将类的class文件读入内存,并为之创建一个java.lang.Class对象。此过程由类加载器完成
(2)链接(Linking)
①验证Verify:确保加载的类信息符合JVM规范,例如:以cafebabe开头,没有安全方面的问题。
②准备Prepare:正式为类变量(static)分配内存并设置类变量默认初始值
的阶段,这些内存都将在方法区中进行分配。
③解析Resolve:虚拟机常量池内的符号引用(常量名)替换为直接引用(地址)的过程。
(3)初始化(Initialization)
执行
类构造器<clinit>()方法
的过程。类构造器<clinit>()方法
是由编译期自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的。(类构造器是构造类信息的,不是构造该类对象的构造器)。当初始化一个类的时候,如果发现其父类还没有进行初始化,则需要先触发其父类的初始化。
虚拟机会保证一个
类的<clinit>()方法
在多线程环境中被正确加锁和同步。
类加载器(classloader)
类加载器的作用
将class文件字节码内容加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后在堆中生成一个代表这个类的java.lang.Class对象,作为方法区中类数据的访问入口。
类缓存:标准的JavaSE类加载器可以按要求查找类,但一旦某个类被加载到类加载器中,它将维持加载(缓存)一段时间。不过JVM垃圾回收机制可以回收这些Class对象。
类加载器的分类(JDK8为例)
JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)
和自定义类加载器(User-Defined ClassLoader)
。
从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。无论类加载器的类型如何划分,在程序中我们最常见的类加载器结构主要是如下情况:
(1)启动类加载器(引导类加载器,Bootstrap ClassLoader)
- 这个类加载使用
C/C++语言
实现的,嵌套在JVM内部。获取它的对象时往往返回null - 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path路径下的内容)。用于提供JVM自身需要的类。
- 并不继承自java.lang.ClassLoader,没有父加载器。
- 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
- 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
(2)扩展类加载器(Extension ClassLoader)
- Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
- 继承于ClassLoader类
- 父类加载器为启动类加载器
- 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
(3)应用程序类加载器(系统类加载器,AppClassLoader)
- java语言编写,由sun.misc.Launcher$AppClassLoader实现
- 继承于ClassLoader类
- 父类加载器为扩展类加载器
- 它负责加载环境变量classpath或系统属性 java.class.path 指定路径下的类库
- 应用程序中的类加载器默认是系统类加载器。
- 它是用户自定义类加载器的默认父加载器
- 通过ClassLoader的getSystemClassLoader()方法可以获取到该类加载器
(4)用户自定义类加载器(了解)
- 在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的。在必要时,我们还可以自定义类加载器,来定制类的加载方式。
- 体现Java语言强大生命力和巨大魅力的关键因素之一便是,Java开发者可以自定义类加载器来实现类库的动态加载,加载源可以是本地的JAR包,也可以是网络上的远程资源。
- 同时,自定义加载器能够实现
应用隔离
,例如 Tomcat,Spring等中间件和组件框架都在内部实现了自定义的加载器,并通过自定义加载器隔离不同的组件模块。这种机制比C/C++程序要好太多,想不修改C/C++程序就能为其新增功能,几乎是不可能的,仅仅一个兼容性便能阻挡住所有美好的设想。 - 自定义类加载器通常需要继承于ClassLoader。
有什么用
我们通过反射可以获取运行时的对象的class对象:
有四种方式:
- 调用类的静态变量class
1 | Class<User> userClass = User.class; |
- 调用类的对象的getClass()方法
1 | User user = new User(); |
- 调用Class类的静态方法forName(类所在路径)
1 | String classUrl="ReflectionDemo.User"; |
- 使用类加载器
1 | String classUrl="ReflectionDemo.User"; |
怎么用
调用了class对象,我们可以使用class对象做非常多的事,例如:
创建对象
无参构造对象:
1 | String classUrl="ReflectionDemo.User"; |
创建带参构造对象:
1 | String UserUrl= "ReflectionDemo.User"; |
获取父类的类对象
1 | String UserUrl= "ReflectionDemo.User"; |
获取所实现的所有接口
1 | String UserUrl= "ReflectionDemo.User"; |
获取类中的属性类(Field)
获取类指定的public修饰的成员变量类以及父类的public所修饰的类
1 | String UserUrl= "ReflectionDemo.User"; |
获取该类以及父类的全部(public修饰)公共成员变量类(已被封装到Field数组中)
1 | String UserUrl= "ReflectionDemo.User"; |
获取类中指定的声明的类(不包含父类,但可以获取private所修饰的类)
1 | String UserUrl= "ReflectionDemo.User"; |
获取类中所有的声明过的类(不包含父类,但可以获取private所修饰的类)
1 | String UserUrl= "ReflectionDemo.User"; |
Field转为toString后的输出形式为(修饰符 类型 全类名.成员变量名)
那么拿到Field我们又可以干什么呢?
- 获取该字段所加的注解类
拿到注解后可以调用注解信息
- 获取修饰该字段的类的类对象
1 | String UserUrl= "ReflectionDemo.User"; |
结果为Int
我们可以获取该类的所修饰限定的字符为
例如我们要获取修饰USer对象的是什么类型
1 | String UserUrl= "ReflectionDemo.User"; |
结果为根据下图可知,修饰User类的修饰符为public
也可以通过获取类中的属性来获取该属性的修饰符的值
1 | String UserUrl= "ReflectionDemo.User"; |
根据上图可知,age,name,id修饰限定符为 private salary为 public
获取类上的指定注解对象
1 | String UserUrl= "ReflectionDemo.User"; |
获取类上的所有注解对象并将其封装到注解数组中
1 | String UserUrl= "ReflectionDemo.User"; |
获取类的指定public修饰的方法
1 | String UserUrl= "ReflectionDemo.User"; |
获取所有public修饰的方法(包含父类)
1 | String UserUrl= "ReflectionDemo.User"; |
输出结果为
和类一样可以调用指定自己声明的方法和所有自己声明的方法在此不再多赘述
当我们获取了类中的方法的类后我们可以做什么呢?
获取该方法的注解(和字段与类一样不多赘述)
获取方法所抛异常类型 (类型、泛型、注解类型 不多赘述)
获取参数个数
1 | String UserUrl= "ReflectionDemo.User"; |
获取所有参数
1 | String UserUrl= "ReflectionDemo.User"; |
获取所有参数类型
1 | String UserUrl= "ReflectionDemo.User"; |
获取返回值类型
1 | String UserUrl= "ReflectionDemo.User"; |
获取到方法了,我们如何执行该方法呢? (invoke)
首先在我们获取方法的时候,我们获取的是,方法名为XXX 参数类为XXX的方法所以我们可以调用该方法并进行执行
1 | String UserUrl= "ReflectionDemo.User"; |
获取方法异常类泛型,参数泛型,返回值泛型
利用反射读取配置文件中的数据
首先我们的配置文件是在本项目的src下
我们可以利用反射中的类加载器来将文件中的信息变成输入流
1 | InputStream resourceAsStream = ClassLoader.getSystemClassLoader().getResourceAsStream(全类名); |
获取配置文件中的配置名来获取值
1 | String fruitName = (String) properties.get("配置名"); |
我们可以直接通过输入全类名获取对象
1 | public <T> T getIntance(String className) throws Exception { |
也可以通过输入全类名和类中的方法名来调用方法
1 | public Object invoke(String classname,String methodName) throws Exception{ |
Lambda表达式
Stream
概述
stream API关注的是多个数据的计算(排序、查找、过滤、映射、遍历等),面向CPU的。集合关注的数据的存储,向下内存的。
Stream API 之于集合,类似于SQL之于数据表的查询
使用说明
- Stream自己不会存储元素。
- stream不会改变源对象。相反,他们会返回一个持有结果的新Stream。
- stream操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。即一旦执行终止操作,就执行中间操作链,并产生结果。
- stream一旦执行了终止操作,就不能再调用其它中间操作或终止操作了。
创建Stream实例对象
根据Collection获取流
collecation.stream();即可生成stream对象
1 | ArrayList<User> users = new ArrayList<>(); |
获取并行流
1 | ArrayList<User> users = new ArrayList<>(); |
根据数组获取流
1 | Integer[] arr=new Integer[]{1,3,4,5}; |
常用方法
Stream的方法被分为两种
终结方法:返回值类型不再是 Stream 接口自身类型的方法,因此不再支持类似 StringBuilder 那样的链式调用。本小节中,终结方法包括 count 和 forEach 方法。
延时方法:返回值类型仍然是 Stream 接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方法均为延迟方法
延时方法
筛选与切片
过滤流(Filter)
可以用于过滤
可以通过 filter 方法将一个流转换成另一个子集流。
1 | public static void main(String[] args) { |
我们可以发现原数组并未发生变化
截断流(limit)
limit(n)——截断流,使其元素不超过给定数量。
用法SQL中的Limit一样不过只能传一个参数
1 | stream.filter(user -> user.getSalary()>5000).limit(1).forEach(System.out::println); |
跳过元素(skip)
skip(n)——跳过元素,返回一个扔掉了前n个元素的流。若流中元素不足n个,则返回一个空流。
1 | User zf = new User(21, "zf", 8000); |
筛选(distinct)
distinct()-—筛选,通过流所生成元素的 hashCode() 和 equals()去除重复元素
1 | User zf = new User(21, "zf", 8000); |
直接给最后一条去除了
映射
如果需要将流中的元素映射到另一个流中,可以使用 map 方法。该接口需要一个 Function 函数式接口参数
案例:将集合中的小写字母转化为大写字母
1 | String[] strings=new String[]{"aa","bb","cc","dd"}; |
输出薪资大于等于5000的员工信息
1 | User zf = new User(21, "zf", 8000); |
排序
sorted
自然排序
找出薪资大于5000的员工的薪资并对其进行排序
1 | User zf = new User(21, "zf", 8000); |
指定排序
倘若我们直接将对象进行排序,在没有对User对象重写Comparator方法的情况下会直接报错,所以我们要指定排序方法
查找出薪资超过5000的员工并对其进行升序操作
1 | User zf = new User(21, "zf", 8000); |
终结方法
匹配与查找
allMatch全部匹配:是否全部集合匹配断言条件
例如:查询是否所有用户的薪资都大于2000
1 | User zf = new User(21, "zf", 8000); |
anyMatch任意匹配:是否存在匹配断言条件的集合
1 | User zf = new User(21, "zf", 8000); |
返回第一个元素
findFirst
返回薪资大于2000且薪资最高的员工的姓名
1 | User zf = new User(21, "zf", 8000); |
执行完findtFirst后不执行.get则会输出调用.get()后会返回实际结果
count
计算流中数据的个数
计算出薪资大于2000的员工数量
1 | User zf = new User(21, "zf", 8000); |
max(comparator())
计算出流中最大的元素
1 | User zf = new User(21, "zf", 8000); |
min(comparator())
返回流中最小元素
返回用户最小薪资
1 | User zf = new User(21, "zf", 8000); |
foreach 遍历集合中的 元素
规约
reduce 将流中的元素反复结合起来得到一个值,返回T
reduce(基础值,规约规则)
求出每月要付给员工的薪资总和(外加没人每月200的社保)
1 | User zf = new User(21, "zf", 8000); |
收集
collect(collector c) 将流转换为其他形式。接收一个collector接口的实现用于给Stream中元素做汇总的方法
collector提供了许多静态方法
toList :从流中生成 List,List 的类型由 Stream 自动判断;
toSet :从流中生成 Set,Set 的类型由 Stream 自动判断;
toMap:从流中生成 Map,key-value 的类型需要自己指定,有两个重载,一个使用默认的 HashMap,一个可以自己指定 Map 类型;
toCollection:从流中生成 Collection,Collection 的具体类型自己指定。
toList 将集合中元素取出整合为一个List例如:
我们要将所有用户的薪资拿出来并装到一个集合中
1 | User zf = new User(21, "zf", 8000); |
这个作用非常广泛,当我们收到一个集合的泛型为User时我们想将集合的每个User的UserId装到另一个集合中用于范围查询时,这时需要Collectors.toList()方法了
toMap() 将集合中的一部分拿出来作为key,选择集合中的对应的另一部分作为value
例如,我们收到了一个USer集合 ,集合中包含着用户的id,年龄、姓名、薪资,但我们向快速地根据ID去查询集合或集合中的某个姓名
我们可以使用toMap()方法
根据用户ID查询用户,将List<User>
转化为 Map<Integer,User>
类型
1 | User zf = new User(21, "zf", 8000,1); |
根据用户ID查询用户姓名
将List<user>
转化为Map<Integer,String>
类型
1 | User zf = new User(21, "zf", 8000,1); |