前言:
文章有点长~ 按 Ctrl + F 即可搜索你想要的内容
本文包含Java基础部分的一些重要知识及基础易错题总结(太简单的内容未包含在内,为的就是干货满满),内容为自己复盘自己学习到的Java基础知识时整理,相信大家读后必能有收获,但由于其中有些问题带着个人对东西的理解难免会有一些错误,希望大家能够指正。若有哪里写的有让你不明白的地方,也欢迎提问哦。
文章经过多天熬夜整理而来,创作不易,怎能忍心不点赞收藏一波!!
Java语言的重要特性:
1.跨平台 (平台: 操作系统)
跨平台: 一次编译,到处运行。
其跨平台特性的关键就在与Java虚拟机(JVM)。Java源码通过编译产生class文件(即字节码文件),再通过JVM解释class文件的信息,然后发出命令给相应的(操作系统)去执行相应的操作,从而达到了一次编译(编译成class文件),到处运行。
2.面向对象
3.Java语言健壮。其强制类型机制、异常处理、垃圾回收是其健壮的重要保证。
4.Java语言是解释性的语言(还有PHP\Javascript等也是),解释型语言的编译后的代码不能直接由机器执行。(编译型语言则其编译后的代码可以直接执行。如:c\c++)
一张图了解jdk、jre、jvm三者间的关系:
源文件(后缀名为.java)在经过Java开发工具java.exe程序编译后生成字节码文件(后缀名为.class)而此时就需要Java虚拟机(JVM)将其解析,然后将相应的命令发给操作系统去执行。
若只是想运行Java程序,则只需安装JRE即可。
关于Open JDK与Oracle JDK的区别:
1.Open JDK三月发布一次,而Oracle JDK是三年发布一次的。
2.Open JDK 是一个参考模型且是完全开源的,而Oracle JDK是Open JDK的一个实现,并不是完全开源的。
3.Oracle JDK 比 Open JDK 更稳定。
4.在响应性和JVM性能方面,Oracle JDK与Open JDK相比提供了更好的性能。
5.Oracle JDK不会为即将发布的版本提供长期支持,用户每次都必须通过更新到最新版本获得支持来获取最新版本。
6.Oracle JDK是根据二进制代码许可协议获得许可,而Open JDK根据GPL v2许可获得许可。
关于安装jdk后配置环境变量的原因:
我们在安装完后大家都知道要配置环境变量,但是却不知道为什么要这么做,那究竟是为什么呢?
对于这个问题,首先我们得知道在cmd窗口执行一个可执行文件的过程是怎么样的:在控制台窗口,如图:
若输入javac这个编译java源码的exe文件(javac.exe),则控制台会在c盘这个路径中寻找是否存在该可执行文件,如果存在,则会立即执行改文件,但若找不到该文件,则会去搜索系统中环境变量Path所保存的路径底下是否存在该文件,若存在则执行,若不存在则会报错。
如果不添加环境变量,则只有在控制台中切换到javac所在的文件中才能执行这一文件,而此时,若要编译的java文件不存在javac所在的文件中,则会导致javac这一exe文件无法编译该java文件。所以最好的就是用在path环境变量中添加bin路径,这样java文件无论存在哪就都可以用javac文件编译了,也就是让控制台无论在哪个路径底下都能找到java开发工具。
那为什么添加JAVA_HOME这一环境变量再把其变量值改为bin所在的路径然后在Path环境变量中写%JAVA_HOME%\bin呢?
其中JAVA_HOME只是其的一个环境变量名,叫什么并不重要,而%JAVA_HOME%中两百分号表示引用,所以这里表示引用JAVA_HOME这一环境变量,而该环境变量的变量值又是bin所在的路径,所以%JAVA_HOME%\bin就也一样表示bin文件夹中javac和java等exe文件所存储的位置了。
那为啥明明可以一步解决直接在Path环境变量中加入完整的bin中文件的路径却还要这么做呢?
其实对bin文件的位置做了修改,则Path环境变量里的值存的那个路径还得修改,但Path环境变量中还有很多其他的东西,对Path路径修改如果不小心改了其他的东西则可能会影响系统其他东西的运行,而自己创建的JAVA_HOME路径专门用于存放bin文件夹的路径则方便以后的修改,防止误改,起到了避免去修改Path环境变量信息的作用。其实简单点说就是把bin中文件的路径单独拎出来,更方便日后的修改。
Java编译运行时要注意的:
一个java源文件可以有在不同类中有多个main方法,但这些类(可以多个类)中只能有一个public类,且在编译时,javac 文件名.java中的的文件名必须与public类类名一致,如果不一致就会报出:类…是公共的,应在…名为… .java的文件中声明。当有一个java源文件中如果各类都有main方法,则在运行时只需,要运行某个类中的main只需 java main所在类类名 即可。
Java编写代码时需注意的:
1.源文件要使用utf-8编码
2.每行宽度不要超过80字符
3.编写时有次行风格和行尾风格,但最好使用行尾风格。
4.写注释。
标识符注意事项:
1、组成元素只能是字母、下划线、数字、美元符$。
2、不能以数字开头。
3、严格区分大小写(Java里不只是标识符,所有的东西都区分大小写)。
4、注意让人能够见名思意。
6、标识符不能是关键字、保留字,但可以含关键字或保留字。
标识符命名规范:
1、类名、接口名:所有单词的首字母大写,其他小写。如:MyDog
2、变量名、函数名:首单词全部小写,其他单词的首字母大写。如:pigWeight
3、包名:全部小写 格式:com.公司名.项目名.业务模块名
4、常量名:全部大写且单词与单词间用下划线分割。如:MY_NAME
关键字:
在Java中有特殊含义的标识符成为关键字,一般用于表示一个程序的结构或者数据类型。变量名、方法名、类名、包名 都不能是关键字。
关于注释:
1.作为一名合格的程序员必须有良好的注释习惯,其作用是:增强代码可读性,方便后期维护。
2.注释包括三种:单行注释,多行注释,文档注释。
其中文档注释在此多提几句:其注释的内容是可以通过java工具javadoc.exe所解析的,操作方式:javadoc -d 目录 -标签(如:version,author等) …… -标签 文件名.java(.java也是属于文件名的部分,但由于有人没明白在此处和上面一些部分就分开了…) 在执行完这句后就可以在该指定的目录中找到index.html查看了(注意若执行的代码中的那个目录还不存在,则会自动创建)
关于注释中要注意的:
1、单行注释可以嵌套使用,但多行注释不行,如下图中可以看到后面的那个e未注释掉,是因为多行注释是从第一个/*开始到找到一个*/之间的内容看成注释部分,因此中间那个/*被视为了注释内容。
常量中的易错点:
1、字符常量:单个字符用单引号引起的常量,诸如:'12'、'ab' 这些都是错误的。
2、字符串常量:用双引号引起来的内容,如:"a"也是
数据类型:
1.String不是基本数据类型
2.存储long类型数据应该在数据后加L(小写l也行,但不建议,因为容易让人看错)
3.存储float类型数据应该在数据后加f(大写F也行)
4.boolean类型占一个字节
5.浮点数可以只有小数位,如 double num = .123;
6.关于浮点数精度问题:浮点数运算时得到的数会不准确,得到的值是一个非常接近推断的值的数(也有时就是推断的数),如:double num1= 8.1/3;此时得到的就不是2.7而是一个非常接近于2.7的数,当一个经过运算的得到的小数与另一个小数比较是否相等时,我们会判断他们的差的绝对值是否小于某个数(这个数根据具体的需求确定),如:double num2 = 2.7; 如果直接num1 == num2 则此处返回的为false,Math.abs(num1-num2) < 0.01; 则返回的为true,此处若业务逻辑是金额,则只需两个差的绝对值小于0.01则可以判断两个金额是相等的。
数据类型转化需要注意的:
1、凡是byte short char类型的数据在运算过程中都会自动转化成int类型的数据进行运算。 如:byte a = 1; byte b = 2; byte c = a+b; 会报错 如:System.out.println('a'+1);输出结果为98
2、两个不同数据类型的数据运算时,结果的数据类型取决于大的那个数据的类型。 如:int a = 1; long b = 2L; int sum = a + b;也会报错。 如:System.out.println('a'+1);输出结果为98
3、byte b = 3;b += 2; 此处 b += 2; 等于 b = b + 2;但是在此处却不会报错,因为该复合运算底层有一个强制类型转换(b++;也不会报错的原因也是底层有一个强制类型转换)
一个整数在默认情况下为整型,但看到下面几句代码,是否会有一些疑问呢?
int i = 5; byte b = i;编译时会出现报错的情况,
但 byte b = 5; 编译时却没有问题。
既然5默认是一个整型,而i也是一个整型,那究竟是为什么只有上面的代码报错而下面的代码却没有报错呢?原因其实是:java编译器在编译时能够读取一个常量的值,而却不能读取一个变量的值。在byte b = 5;这里5是一个常量,编译器能够读取到其值,检测到其值能够被byte类型存下是则能通过编译,而byte b = i;时此时i是一个变量,编译器只能检测到它是一个int类型的值而不会知道值具体是多少(在这我们还应知道变量在编译时其实是不会开辟内存空间的,只有在java虚拟机运行到该语句时才会开辟内存空间存储那个值),就会在编译时报错显示精度可能会丢失。
基本数据类型与String类型的转换:
int i = 1;①String str = i + ""; ②String str1 = Integer.toString(i);③String str2 = String.valueOf(i);
但如果②方法中如果i本身是是包装类的类型则是:Integer i = 1; String str2 = i.toString();即可
字符串类型转基本数据类型,用基本数据类型包装类的parseXXX方法,如:int i = Integer.parseInt("54");但如果要String类装包装类则Integer integer = Integer.parseInt("54");或者是 Integer integer = new Integer(str);其中str要为数字的字符串
顺便多说一个,提取字符串中某个字符的方法: 字符串(或字符串变量名).charAt(要提取字符所在位置); 如:"hawa".charAt(0); //h
转义字符需注意的:
在Windows操作系统下只有\r\n一起用(且顺序也要先\r后\n)才能实现回车换行,其他操作系统只需\n即可。
算术运算符中要注意的例子:
int i = 0;i = i++; System.out.println(i);输出结果为0。
要知道此题答案需要知道jvm虚拟机的后自增原理。i++;这一句在jvm虚拟机中会先创建一个临时变量,int temp = i; 然后再i = i +1;最后如果有赋值语句时再把temp给出去这里则是 i = temp; 虽然这波操作看起来有点奇怪,但是其实这才符合算术运算符的优先顺序,而以往刚开始学习说的先使用后++反而有点问题,因为++的优先级是高于=的,如果按大多数的地方理解后++就是先使用后++的话 这里就不太符合优先级了。
同理,后置--也会有同样的现象。
复合赋值运算符需注意:
byte b = 1; b = b+1;则会报错 而 byte b = 1; b += 1;则是不会报错的,因为java编译器会自动强制类型转换
其他复合赋值运算符也有相同现象。
位操作符中一条重要规律:
如果一个操作数异或另一个数两次,那结果还是原来那个数据。(可用于加密文件)
位操作符有关的两道经典题:
不创建临时变量交换两个变量的值。int a = 1; int b = 2; 解法一:a = a + b; b = a - b; a = a - b; 缺点: 两个int 类型的值相加放在int中可能会溢出
解法二:a = a ^ b; b = a ^ b; a = a ^ b; (用到了上述重要规律) 经过a = 1 ^ 2;后第二句相当于b=1 ^ 2 ^ 2 则b换成了1,而第三句相当于 a = 1 ^ 2 ^ 1 则a换成了2 缺点:逻辑不清晰
取出一个数二进制位的后六位
如: 00000000 00000000 00000000 00011011 只需 &00000000 00000000 00000000 00111111 取某个数二进制的后a位只需与上一个后a位全是1,其他位全是0的二进制
移位操作符及相应经典面试题
<< 左移:对应二进制位向左移动,末位补0
注:正数向左移动n位相当于乘以2的n次方(除了首位去掉的是1或却掉后是1的情况)
>> 右移:对应二进制位向右移动,首位补充符号位
注:正数向右移动n位相当于除以2的n次方
>>> 无符号右移(逻辑右移):对应二进制位向右移动,首位补0
移位操作符的特点:效率高
以最高效率算出2乘以8的值:答案2<<3
一个负数得到其绝对值的方法:
1.负数补码,-1,所有位都取反,得到其绝对值,2.负数,所有位取反,+1,得到其绝对值
例如:得到-1的绝对值:int a = -1; (~(a)) + 1 的结果即是-1的绝对值了。在有些地方也可以看到(a^( a>>31)) + a>>>31; 也是可以的,其中a>>>31位,a是负数的话结果就是1,而a>>31位得到的就是32个1的二进制序列,而一个数^一个全为1的二进制序列得到的结果就是相当于 所有位都取反。但后面这种的好处就是它无论是对正数还是负数都能得到其绝对值,但前面那种方法只能对负数取其绝对值。
关于==号
1、如果==号两边是基本数据类型,则比较的是两个内容是否相等
2、如果==号两边是引用数据类型时,则比较的是两个数据的地址
关于switch语句中需要注意的:
1.switch语句适用的数据类型:byte、short、char、int、String、枚举类型
2.case后只能接常量或者常量表达式(如:'a' + 1)(如果switch中的类型是枚举类型,那么此处写枚举值即可 注意:不需枚举类类名.枚举值,只需写枚举值即可)
3.switch表达式中的类型必须与case常量的类型一致,或者其中一个是可以自动转化为另一个才可以。
//基本格式
switch (expression) {
case value1:
break;
case value2:
break;
... case valueN:
break;
default:
}
关于打印乘法口诀表时怎么对齐:
我知道的有两种,若还有其他的,欢迎评论区留言: System.out.printf("%d*%d=%-2d ",i,j,i*j); //%-2d表示输出2列左对齐
System.out.print(i + "*" + j + "=" + i*j + "\t"); //\t是水平制表符,用于对齐
关于方法调用过程中内存的变化:
在方法调用时,就会在栈区开辟一片空间(栈空间),当方法执行完毕时该方法的栈空间就会销毁,main方法是最先开辟,最晚销毁的。
关于方法需要注意的:
1.方法里面不能定义方法(方法不能嵌套定义)。
2.若方法中传递过来的是引用类型,要注意方法中判断该引用类型的数据是否为null,否则会导致空指针异常,当传入的是数组这种引用类型时最好还判断 arr.length > 0; 不然下面 arr[索引值] 时会导致越界访问。 以下举个例子方便大家记忆:
//未进行任何优化得到一个数组中的最大值
public int max(int[] arr) {
//如果arr.length = 0 则arr[0]会出现越界访问
//如果arr为null则arr[0]及arr.length会出现空指针异常
int max = arr[0];
for(int i = 0; i < arr.length; i++) {
if(max < arr[i]) {
max = arr[i];
}
}
return max;
}
//返回类型用Integer使得如果arr为null或arr.length为0时可以返回null
public Integer max(int[] arr) {
//避免出现越界访问及空指针异常
if(arr != null && arr.length > 0) {
int max = arr[0];
for(int i = 0; i < arr.length; i++) {
if(max < arr[i]) {
max = arr[i];
}
}
return max;
} else {
return null;
}
}
关于方法重载:
1.方法名一样
2.方法的参数列表不一样
3.方法的返回类型及访问修饰符任意
注意:只有返回类型不同不能算重载,要的是参数列表不同,返回类型同不同无所谓
全局变量与局部变量的作用域:
局部变量:即除属性以外的变量,作用域为定义该变量的语句所在的代码块。
全局变量:即属性,作用域为所在类体。若在其他类中创建了该全局变量所在类的实例化对象,则可以通过该对象在其他类中使用。
关于局部变量和全局变量要注意的:
1.局部变量没有默认值,未赋值不可以直接使用。但全局变量(属性)有默认值,可以直接使用。
2.局部变量不能添加访问修饰符,但全局变量可以。
for循环需注意的:
for(int i = 0; 条件判断部分; i++) {} 1.若在for循环的括号内定义i ,则出了for循环体i则不能使用。 2.for循环括号内的内容除了条件判断的部分和分号以外都可以写在外面(上面的 int i = 0 与 i++ )可以是 int i = 0; for(;条件判断部分;){ i++; } 3.如果条件判断部分不写,则是表示死循环。
break与continue在嵌套循环中如何作用于不是那个默认的当前循环:
只需在循环前(在循环的上一行也行)添加标签名(命名只要符合标识符的命名即可)加上冒号,然后在break/continue的后面加上标签名即可作用于想要作用的循环。(平常尽量不要使用标签,因为可读性会降低)
以下举一个简单的例子,执行后发现,只会打印一个“略略略”:
outer:for(int i = 1; i<=9; i++) {
inner:for(int j = 1; j<=i; j++) {
System.out.println("略略略");
break outer;
}
}
//或者像下面这样也一样
outer:
for(int i = 1; i<=9; i++) {
inner:
for(int j = 1; j<=i; j++) {
System.out.println("略略略");
break outer;
}
}
break和continue后面不能直接跟语句:
因为如果直接跟语句,即break/continue与其后面的语句的执行条件是一样的,其后的语句是在break/continue后面执行,但是break/continue以后后面的语句是没有机会执行的,故其后紧接着的语句相当于废的语句。下面列举一个简单例子,其在编译时是会出错的。
for(int i = 0;i<2;i++) {
break;
System.out.println(i);
}
创建一维数组相关重要知识点:
动态初始化:
1.声明数组的同时创建数组:int[] arr = new int[3];(也可以 int arr[] = new int[3];),但建议写前一个,因为arr的数据类型就是int[],写在前面就跟声明其他数据类型一样写法(数据类型 变量名) 左边int表示只能存储int类型的数据,[]表示是一个数组,arr是变量名 右边new是创建一个对象的关键字,int表示该数组对象只能存放int类型,[]表示数组类型,3表示数组能存放的元素个数 2.先声明数组再创建数组:int[] arr;(也可以 int arr[];) arr = new int[3]; 这其实就是将上面那种声明的同时创建数组的方法拆开成了两步,效果是一样的。但在这里还要知道,在创建这个数组前是不会给这个数组分配空间的。
要真正了解数组还需知道数组在内存中是怎么存的,以下这张图能帮助我们很好的理解。 其中数组名arr存放在栈中,而new关键字创建的数组对象则存放在堆中。
静态初始化:int[] arr = {元素};(同样也可以int arr[] = {元素};)这种初始化方法在内存中产生的效果是一样的。但注意不可以 int[] arr; arr = {元素};
两种初始化方法看需求选择即可(一般刚开始就确定了数据则使用静态初始化)。
数组中要注意的:
1.如果创建数组后没有赋值,它本身是有默认初始值的:基本数据类型byte 0, short 0, int 0, float 0.0, double 0.0, char '0', boolean false, 引用数据类型 如String,Object等的数组默认值都为null
2.注意索引值不要越界。
3.注意还有一种不常见的创建数组的方式:int[] arr = new int[]{1, 2, 3}; 且要注意此时不能指定个数如:int[] arr = new int[2]{1, 2};是错误的。
如何给原始数组是静态初始化的数组扩容?
创建一个大的数组,并将原来数组的内容拷贝到该数组,最后使原来数组的引用的指向新的数组。
几种排序的方式:
1.直接排序(选择排序)
public static void main(String[] args) {
int[] arr = {1, 3, 9, 6, 2, 7, 5};
selectSort(arr);
for(int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
public static void selectSort(int[] arr) {
int temp = arr[0];
for(int i = 0; i < arr.length - 1; i++) {
for(int j = i + 1; j < arr.length; j++) {
if(arr[j] > arr[i]) {
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}
}
2.已进行过优化的冒泡排序
public static void main顺序冒泡(String[] args) {
int[] arr = {6, 9, 4, 2, 5, 7, 8, 3, 1};
for(int j = 0; j < arr.length-1; j++) {
boolean flag = true;//搞个标记以实现优化(看在下趟排序时是否进行了交换,如果没有则说明不用再排了)
for(int i = 0; i < arr.length-1-j; i++) {
if(arr[i] > arr[i+1]) {
int temp = arr[i];
arr[i] = arr[i+1];
arr[i+1] = temp;
//如果在一次冒泡排序过程中经过了交换
flag = false;
}
}
//判断是否需要继续排序
if(flag) {
break;
}
}
for(int i : arr) {
System.out.print(i + " ");
}
}
上述第一种方法是从大到小,第二种方法从小到大排序,但这两种方法各种都可以完成从大到小或从小到大排序,此处就不赘述了。
在有序数组中插入一个数,使插入后任然有序
public static void main(String[] args) {
int[] arr = {10, 12, 23, 90};
//需要添加的元素
int addNum = 23;
//搞个新数组以实现扩容
int[] arrNew = new int[arr.length+1];
boolean flag = true;//用于判断是否已经添加
for(int i = 0, j = 0; i < arrNew.length; i++) {
if(flag) {
//使比添加的元素更小的元素在前面
if(arr[j] < addNum) {
arrNew[i] = arr[j];
j++;//如果在for循环中j++的话,当走下面else添加addNum时j也会++,下面再提
} else {
arrNew[i] = addNum;
flag = false;
//如果这里也有j++的话,那么就会漏掉此时的arr[j]
}
} else {
arrNew[i] = arr[j];
j++;
}
}
arr = arrNew;
for(int i : arr) {
System.out.print(i + " ");
}
}
或者下面这样:
public static void main(String[] args) {
int[] arr = {12, 45, 67, 89};
int addNum = 34;
int i = 0;
int[] arrNew = new int[arr.length+1];
for(i = 0; i < arrNew.length; i++) {
if(arr[i] < addNum) {
arrNew[i] = arr[i];
} else {
break;
}
}
arrNew[i] = addNum;
for(int j = arrNew.length-1; j > i; j--) {
arrNew[j] = arr[j-1];
}
arr = arrNew;
for(int num : arr) {
System.out.print(num + " ");
}
}
二分(折半)查找法:
import java.util.*;
class Demo {
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6,7,8,9};
Scanner scanner = new Scanner(System.in);
int num = scanner.nextInt();
int a = findNum(arr,num);
if(a != -1) {
System.out.println("找到了,索引值为" + a);
}else {
System.out.println("没找到");
}
}
public static int findNum(int[] arr,int a) {
int left = 0;
int right = arr.length-1;
int mid = (left+right)/2;
while(left<=right) {
if(a>arr[mid]) {
left = mid+1;
mid = (left+right)/2;
}else if(a<arr[mid]) {
right = mid-1;
mid = (left+right)/2;
}else {
return mid;
}
}
return -1;
}
}
二维数组:
二维数组就是一个存放数组的数组。可以把二维数组看成一个一维数组里每个元素是一个一维数组。其中每一个一维数组的元素个数可以不同。二维数组跟一维数组一样,可以先声明再创建。
int[][] arr = new int[2][3];创建的是一个二维数组,但实际上相当于创建了一个含两个元素的数组,其中2就是这个二维数组的元素个数(用arr.length可得到),这两个元素又分别是一个含有三个元素的数组(二维数组中每个一维数组的元素可以用arr[i].length得到)。
上面的int[][] arr = new int[2][3];(写成int arr[][] = new int[2][3];也是一样的,还可以写成int[] arr[] = new int[2][3]; 但自己写最好还是别这么写)如果其二维数组中的一维数组的元素个数不确定或不同,则不写, 如:int[][] arr = new int[2][];此时这句的意思是创建二维数组,并给定了二维数组中一维数组的元素,每一个一维数组通过arr[i] 来指向,但此时并没有给里面的一维数组开辟空间,arr[i]的内容为null。如果要对其初始化可以arr[i] = new arr[元素个数],但注意不可以arr[i] = {元素};,这里与一维数组的先声明再创建类似,一位数组不可以先声明,再静态初始化。
注意:可以 int[][] arr = {{1, 2}, {1, 2, 3},{100}}; 但不可以int[][] arr = {{1, 2}, {1, 2, 3},100}; 大括号不能省,省了类型就是整型而这里要的是 int[]类型的数据。
下面的图能让我们能更好的了解二维数组:
面向对象:
类是引用数据类型,类是对象的模板,对象就是类的具体实例。
一道新手很可能会错的题:问输出结果?
class Demo1 {
public static void main(String[] args) {
Girlfriend beauty1 = new Girlfriend();
Girlfriend beauty2 = new Girlfriend();
beauty2 = beauty1;
beauty1.name = "白富美";
beauty2.name = "矮穷矬";
System.out.println(beauty1.name);
}
}
class Girlfriend {
String name;
public void kiss() {
System.out.println("给你一个亲亲");
}
}
Girlfriend beauty1 = new Girlfriend();
Girlfriend beauty2 = new Girlfriend(); 虽然分别都在堆区创建了新的对象,但是 beauty2 = beauty1; 使得beauty2也变成了指向Girlfriend beauty1 = new Girlfriend();创建的那个对象而不是指向原来自己指向的那个对象,故输出结果为:矮穷矬
注意:类创建对象的在jvm中的步骤是,如Girlfriend beauty1 = new Girlfriend();先在方法区加载Girlfriend类的信息(当反复用到Girlfriend类创建对象时不会反复加载,只会在第一次用到这个类时加载一次),在堆中分配空间创建该对象并完成对象的初始化(先进行默认初始化->显式初始化 -> 构造器初始化),然后将该对象的地址赋给栈区的 beauty1 中。
关于默认初始化、显式初始化、默认初始化:
下面代码中,创建对象的那一句执行时,会先在方法区加载Person类信息,再在堆中创建该对象,首先会对对象的age属性进行默认初始化,int 类型默认值为0 ,也就是说age先为0,再显式初始化 int age = 10; 则age变成了10,最后再构造初始化this.age = age; 使得age变成了9。
public class Test {
public static void main(String[] args) {
new Person(9);
}
}
class Person {
public int age = 10;
public Person(int age) {
this.age = age;
}
}
关于访问修饰符一定要记住的一张图:
此图是在学习时截取,其中有部分做了修改更方便记忆。
类的访问修饰符需要注意的:
类的访问修饰符只能是:public 或者 默认
类的五大成员:
属性、方法、构造器、代码块、内部类。
JavaBean是啥?
标准的 JavaBean 需满足哪些条件?
- 成员变量使用 private 修饰
- 提供每一个成员变量对应的 setXxx() / getXxx()
- 提供一个无参构造方法
关于类什么时候会被加载:
1.某类第一次创建对象时会被加载。 2.该类某子类第一次创建对象时会被加载。(也就是说创建子类对象时会先加载父类再加载之类) 3.直接使用该类调用该类的static成员时会被加载(即: 类名.静态成员 时会加载该类。注意:也同样会先加载该类的父类)。 4.一个类只会加载一次。
关于类中含有String类型的属性时内存的情况:
用上面的例子来说明:Girlfriend beauty1 = new Girlfriend(); beauty1.name = "白富美"; 其中在给beauty1的name属性赋值时,jvm会去方法区的字符串常量池找是否已经存在"白富美",如果存在,则将该常量的地址给堆中beauty1对象的name,如果不存在则在字符串常量池中创建后再给name。下面的这张图能帮大家更好的理解:
两个创建对象时不理解到位容易错的:
System.out.println(new Girlfriend() == new Girlfriend()); 此处输出结果应为false,要知道原因,得知道两个知识点:1.==两边若是引用数据类型,则比较的是两个数据的地址。2.new关键字每创建一个对象都会在内存中开辟一片新的空间。因为两个new开辟的空间不是相同的,地址自然也就不同,所以输出的结果为false。
new Girlfriend().name = "白富美"; System.out.println(new Girlfriend().name);输出结果为null,原因是上述的第二个知识点及String类型的成员变量默认初始值为null。
关于创建字符串对象时容易错的:
String name1 = "白富美";
String name2 = "白富美";
String name3 = new String("白富美");
String name4 = new String("白富美");
System.out.println(name1 == name2);//true
System.out.println(name2 == name3);//false
System.out.println(name3 == name4);//false
System.out.println(name3.equals(name4));//true
要知道上面代码打印结果为什么会这样我们应该知道:
String name1 = "白富美";这种创建字符串对象的方式创建字符串时,jvm会先到方法区的字符串常量池中查看是否已经存在“白富美”这个字符串,如果没有,则会在字符串常量池中创建然后把这个常量的地址给name1;如果有的话,就直接把该常量的地址给name1
String name3 = new String("白富美"); 这种字符串对象对象的创建方式时,jvm同样会先到方法区的字符串常量池中查看是否已经存在了该字符串,如果没有,则会在字符串常量池中创建,然后拷贝一份把其给堆区创建字符串对象,最后在把该堆区对象的地址给name3 ;如果有的话,jvm则会直接将其拷贝然后给堆区创建字符串对象,最后在把该堆区对象的地址给name3
关于equals 和 ==
== 比较基本数据类型时比较的时大小是否相等,比较两引用类型数据时是比较其地址,要注意,如果比较的是数据类型不同引用时,则会编译报错。
equals只能用于比较引用类型,默认情况下比较两引用类型的地址(此时比较数据类型的引用是可以的),但String类重写了equals方法,所有在用比较两字符串类型数据时是比较两字符串的内容是否相同。Integer也同样重写了……,像这样,如果写的类想要比较的内容不是默认的比较地址时就对其重写就好了。
知道以上几个知识后,答案就变得显而易见了,这里就不赘述了。
关于toString方法需要知道的:
toString方法,默认返回 值为: 全类名(包名及类名)+@+哈希值的十六进制
当直接输出打印一个对象时,打印的为toString方法的返回值,即System.out.println(对象的引用);的结果为该对象所属类的toString方法返回值。
(提一句,内部类的全类名是:包名.类名$内部类类名)
字符串长度获取的方法:
字符串.length(); 注意要与数组的区分,字符串是通过方法获取,而数组是直接通过属性。
匿名对象的用处:
一、调用该类的一个方法且调用后该对象无需再次使用
当只需调用该类的一个方法且调用后该对象无需再次使用时,用创建匿名对象的方式调用更佳。原因主要有两个:原因一比较容易看出的就是可以简化书写,两句变一句它不香吗? 原因二是:如果用普通的创建方式, 如:Girlfriend girl1 = new Girlfriend();girl1.kiss();这样调用的话,这里声明了一个局部变量girl1,而局部变量要等其生命周期结束(出其作用域)后才能空出那片内存, 但如果直接:new Girlfriend().kiss()的话这个创建的匿名对象就会直接成为一个没有变量指向的废物对象,会等待java中的垃圾清理器不定时清理,能为后面的代码腾出更多的空间。
二、作为实参传递数据
当某方法需要传递一个该该类的对象而有不需一个具有完整属性的对象时可直接把该匿名对象当参数传过去即可。为方便大家理解,就随便举个例子,假设有一个touch(参数);方法我们可以 Girlfriend girl1 = new Girlfriend();然后touch(girl1);但是如果你只是单纯的想要touch一个Girlfriend类的对象而后面都不再需要她了,只是单纯的需要这么个对象让你touch, 直接touch(new Girlfriend);就完成了上面两行代码完成的东西,而不必去额外声明一个变量作为参数。
封装的应用场景及作用:
应用场景:1.如果一个属性别人可以直接访问或赋值。
2.实体类(用于描述一类事物的类称为实体类)的成员属性(成员变量)一般都会封装起来。
作用:1.提高了数据的安全性。
2.操作简单。
3.隐藏了实现过程。
构造方法(构造器):
1.构造方法名与类名一致且无返回值。
2.构造方法并不是可以单独调用,而是在创建对象时由jvm自动调用。
3.构造方法无返回值,在定义构造方法时也无返回类型(注意:无返回类型 不等于 void)
4.构造方法的修饰符可以是默认或public、protected、private
5.若没有定义构造方法,系统会默认自动生成一个无参的构造方法
6.如果自己定义了构造方法,默认的无参构造方法将会被覆盖,如果自己未再定义一个无参构造方法,就不能再使用无参构造方法。(即,如果只定义了含参构造方法;则 new Girlfriend(); 中未传参会报错)
关于构造方法及封装需要注意的:
如果将属性封装起来了,那么构造方法在赋值时必须调用用封装写的那些set方法,不然在创建对象时传参调用构造方法时初始化就不会有封装搞的set方法里的约束条件在创建对象时就有效。要向下面代码一样在构造方法中 this.set方法来初始化
class Person {
private String name;
public Person(String name) {
//不要直接下面这样
// this.name = name;
//而应调用set方法
this.setName(name);
}
public String getName() {
return name;
}
public void setName(String name) {
if (name.length() >= 2 && name.length() <= 6) {
this.name = name;
} else {
System.out.println("输入错误");
}
}
}
构造代码块(普通代码块):
构造代码块是放在成员属性的位置上,作用是给所有创建的对象进行统一的初始化,将重载的构造方法中重复的代码放到代码块中以减少无用的重复。
将重载的构造方法中都有的代码提取成构造代码块后,无论使用哪个构造方法,在构造方法中的语句执行前都会先执行构造代码块中的语句。且构造代码块每创建一个对象都会执行一次。(因每次创建对象都会调用构造方法)
public class Test {
public static void main(String[] args) {
new Person();
new Person("大傻");
}
}
class Person {
private String name;
{
System.out.println("我是🐖");
System.out.println("我真的是猪");
}
public Person() {
System.out.println("无参构造");
}
public Person(String name) {
this.name = name;
System.out.println("含参构造");
}
}
输出结果如下:
静态代码块:
被static修饰的代码块叫做静态代码块。
关于普通代码块和静态代码块要注意的:
1.静态代码块是随着类的加载而执行的。类信息加载时,先加载静态的成员,再加载非静态成员(此处还需要知道类在什么时候会被加载,前文中有写,此处不再赘述。)
2.静态代码块与静态属性的优先级是相同的,也就是说谁写在前面就会先加载谁。
3.普通代码块与非静态成员的先默认初始化后显式初始化的也是按在类中谁写在前面就先执行的。
4.构造方法中隐藏了super()及构造代码块语句,其中super()语句在前。
5.综合以上几点创建一个子类对象时,执行顺序排序为:父类静态代码块或静态属性——>子类静态代码块或静态属性——>父类普通代码块或或普通属性的初始化(先默认初始化后显式初始化)——>父类构造方法中语句——>子类普通代码块或或普通属性的初始化——>子类构造方法中语句 (其中默认初始化和显式初始化在前文中提过,忘了可以直接ctrl+f回去看)
局部代码块的作用(不常用但也还是要知道):
缩短局部变量生命周期。因为定义在局部代码块中的成员出了代码块生命周期就结束了。
类中构造代码块、构造方法及成员变量显式初始化的执行先后顺序(新手易错):
1.构造函数是放在显式初始化及构造代码块之后执行的。
2.成员变量的显式初始化及构造代码块的执行顺序是按编写顺序实现的。
以下举一个例子,若打印 i 的值,则会发现结果为2:
class Girlfriend {
{
i = 1;
}
int i = 2;//成员变量的显式初始化
}
从上面的例子中仔细的人可能会想,为啥这里的 i 在声明之前就使用了而编译时却未出现报错的情况,这里是因为:java编译器在编译过程中会将构造代码块及成员变量初始化的那一部分放到构造方法中实现(即会在构造函数那放上 i =1;i=2; 这里其实可以通过将编译器编译出的class文件反编译来查看)。而两者放到构造函数中的顺序为编写时的顺序,所以成员变量初始化与构造代码块的执行顺序是按编写顺序的。
this关键字:
1.一个类中若存在同名的成员变量与局部变量时,在方法内部默认是访问该方法内的局部变量的数据,但可以通过this关键字访问成员变量的数据。当一个方法中没有该成员变量时无须加this访问的也是那个成员变量,实际上虽然你没有加this,但是在java编译器编译时会默认加上this。
2.一个构造方法中调用本类中另一个重载的构造方法时用this代替另一重载的构造方法的名字即this(参数);且该调用必须位于第一个语句。
3.若在本类中无this调用的成员,则会在父类中找。
static修饰成员变量:
static 修饰成员变量,将成员变量由原来堆区存放对象的地方放到方法区内存中的静态数据共享区,且只会存一份数据。即所创建的所有对象的这一属性都共享此同一个数据。(JDK8之后为放到堆中的独立于本类创建的对象的一个class对象里面,但同样是本类所有的对象都共享这份数据)static修饰的成员变量会随着类的加载而产生,所有不用创建对象也可以直接通过 类名.静态成员名 这种方式访问
作用:若某一属性的内容是所有对象都相同的时候,如果不用static修饰的话,每一个存放这一属性的成员变量都会占有一片内存,如果创建的对象多的话则会大大地浪费内存。用static修饰的话就只会占用一块内存了。
为了方便理解,以下这个例子能帮助我们更好的理解,打印结果全为 和尚班 原因就是static修饰的成员是只占一块内存的,一旦有一个对象对该属性进行修改,其他对象的这一属性也就被修改了。
class Demo1 {
public static void main(String[] args) {
Student student1 = new Student("张三");
Student student2= new Student("李四");
student1.class = "和尚班";
System.out.println(student1.class);//和尚班
System.out.println(student2.class);//和尚班
}
}
class Student {
String name;
static String class = "终极1班";
public Student(String name) {
this.name = name;
}
}
非静态成员变量与静态成员变量的区别:
1.数据数量上的区别:
非静态成员变量是在每个对象中都维护一份数据
静态成员变量只会在方法中维护一份数据
2.访问方式上的区别:
非静态成员变量只能使用对象进行访问
静态成员变量可以通过对象、类名进行访问
3.存储位置上的区别:
非静态成员变量存储在堆区中
静态成员变量是存储在方法区中的(JDK8后随着类的加载而在堆区的class对象中)
4.生命周期的区别:
非静态成员变量是随着对象的创建而存在随着对象的消失而消失
静态成员变量是随着class文件的加载而存在随其消失而消失,但class文件一加载就要等jvm不再运行时才会消失。
5.作用上的区别:
非静态成员变量作用是描述一类事物的属性
静态成员变量的作用是提供一个共享数据给所有对象使用。
static修饰的静态方法需注意:
1.静态方法的访问方式也有两种:可以直接 类名.方法名
2.static修饰的静态方法可以直接访问静态成员(成员方法及成员变量)但不可直接访问非静态成员,但可以通过创建对象,在通过对象去访问。
3.非静态方法可以直接访问静态以及非静态的成员。
4.静态方法不能出现this以及super关键字。 5.如果一个方法没有直接访问非静态的成员,那么即可使用static修饰该方法。常用于工具类的方法(方便直接用 类名.方法名 调用该方法)。 6.普通方法和类方法都是随着类的加载而加载到方法区 7.static修饰的方法不能被重写
为什么会出现上述要注意的问题?
关键的因素:静态数据在jvm解析class文件的时候就会加载到(方法区的静态数据共享区)内存中,而非静态数据要在创建对象后才会加载到(堆区)内存中。静态数据是优先于对象存在的。
1.静态函数不可直接访问非静态成员,而非静态函数为什么可以直接访问静态及非静态成员的原因是:静态数据是先于非静态数据在内存中的,所以静态函数不可直接访问非静态成员,而非静态函数为什么可以直接访问静态及非静态成员。还有一点就是静态函数可以直接 类名.函数名 调用而那时可能都还没有创建对象,非静态成员都还未在内存中,自然不能调用。
2.关于为什么不能出现this和super关键字来说其实比较好解释。拿this关键字来说,从静态函数的调用方式即可看出,因为它可以直接 类名.函数名 这样调用,而函数中的this表示的是调用该函数的对象,但仅 类名.函数名 就可以调用说明不用创建对象就能调用,那样的话对象都没有this指代啥对象呢?所以肯定是不能在静态函数中有this和super关键字的。
注意:上面说的是不能直接访问,但并不是不能访问,有对象的话可以访问。
不能直接访问的意思如下图:
但是在有对象的情况下是可以访问的,如图:
main方法的那些事:
为什么main方法要那样设计呢? public static void main(String[] args) {}
public:保证该类在任何情况下,jvm都对该方法可见。
static:被static修饰可以使得该方法直接通过类名来调用,而在此使用static可使得jvm无须创建一个对象即可调用main方法,而当在这个时候如果main方法所在类创建了含参构造方法却没有创建 无参构造方法,那创建对象时jvm将不知道传啥参数合适。
void:无返回值,因为有返回值返回的也是给jvm,根本没有啥作用。
main:只是一个方法名,是给jvm识别的一个比较特殊标识符。是程序的入口。
String[] args :一个String类型数组的形参,考虑某些程序在启动时需要一些参数。给这个数组添加元素的方法如下图,但一般不用。
在IDEA中如果要向main方法中传参则按以下步骤:
在红色方框部分输入要传入的字符串元素即可,元素间用空格隔开
单例设计模式:
作用:保证一个类在内存中只有一个对象。 饿汉式单例设计模式实现过程如下:
class Single {
private static Single single = new Single();//声明一个该引用类型变量并创建一个对象
private Single() {} //私有化构造函数使不在本类中不能创建对象
//提供一个公共静态方法获取本类对象
public static Single getInstance() {
return single;
}
}
懒汉式单例设计模式实现过程如下:
class Single {
private static Single s;//先声明这样一个引用类型的变量但先不创建对象
private Single() {}//私有化构造函数使不在本类中不能创建对象
//提供一个公共静态方法获取本类对象,获取之前判断是否已经创建了本类对象,如果没有创建,则先创建
private static Single getInstance() {
if(s == null){
s = new Single();
}
return s;
}
}
饿汉式单例模式虽然可能会存在先创建了对象但后面用不上而浪费内存的问题,但懒汉式单例模式却可能存在线程安全问题。
关于这里的线程安全问题:存在的可能是多个人同时执行此代码,但CPU在一定时间内却只能给一个线程用,若线程一在执行完判断语句后CPU被线程二拿走并执行了if(s == null) {s = new Sing();} 然后CPU回到了线程一手里那么他就还会再创建一个对象,就不符合只创建一个对象了。不过这并不代表懒汉式单例设计模式没用,在多线程学完后上面这个问题其实是可以解决的。
继承:
1.子类会继承父类所有属性(包括private修饰的属性,但如果要访问只能通过父类提供public方法) 2.会继承除构造方法以外的所有方法。
关于继承中子类创建对象时对父类构造函数的调用:
父类的构造方法不能被继承(继承也没用,子类创建对象时又不是以父类类名创建对象(就不会调用父类的构造方法),那继承的意义何在),但在子类创建对象时父类构造方法会被调用。
在子类创建对象时,无论是使用含参还是无参的构造方法,都会默认去调用父类的无参构造方法,如果父类没有无参构造方法(含参构造方法把无参构造方法覆盖了)则必须在子类构造方法中通过super来指定调用父类的哪个方法来完成对父类的初始化。(且注意父类构造方法的调用不限于直接父类,会一直到顶级类,即Object类)
调用父类构造方法的目的就是为了初始化从父类那继承的属性。默认情况下调用的是父类的无参构造方法,即默认不在创建对象时初始化从父类那继承的属性,而如果需要在创建对象时就初始化从父类那继承的属性则需要super关键字来在子类的含参构造中调用父类的含参构造方法来初始化从父类继承的属性。(其实用this关键字在子类的含参构造中也可对从父类继承的属性进行初始化,但因为那是从父类继承的属性,所以用父类的构造方法去初始化更合理) ,此时不会再调用父类的无参构造方法。(创建一个对象时一个类的无参构造和含参构造一次只会调用一个,当传了参的时候就只调用含参的,不传参时就只调用无参的。)
super关键字的作用: 1.如果子类和父类没有同名的成员时,可通过super关键字可指定访问父类中的成员(成员属性及成员方法),但注意不可以访问private修饰的成员。如果子父类含有同名成员时,可通过super访问父类成员。还有一点要注意的就是,要用super访问父类成员,若父类没有,则会继续往上找直至Object类。 2.创建子类对象时,默认调用父类无参构造方法。若父类没有无参构造方法(写了含参构造把无参的给覆盖了),则需要,通过super关键字手动在子类的构造方法中指定调用父类的构造方法。(此时super关键字必须位于子类构造方法中的第一个语句)
final关键字:
final关键字的作用:
1. final关键字修饰一个基本类型变量时,该变量不能重新赋值。通常都会在定义常量时这样用,如:public final double TAX_RATE = 0.1;
2. final修饰的成员变量一定要赋值,可以直接定义时赋值,也可以在构造方法或者构造代码块中赋值,但如果有final修饰又有static修饰时,则只可以在定义时赋值或者在静态代码块中赋值。final 修饰局部变量时一定要在使用前进行赋值。
3. final关键字修饰一个引用类型的变量时,该变量不能指向新的对象,但是其所指向的对象的内部成员是可以被修改的。
4. final关键字修饰方法:该方法不能被重写,但会被继承。
5. final修饰一个类时,该类不能被继承,但可以实例化。但类被final修饰时,类中的方法一般不会用final修饰,因为此时已经不可能被重写了。包装类及String类都是final修饰的,不能被继承。
6 . final与static共同使用往往效率更高(因底层编译器做出了优化处理,在使用类调用该静态成员时不会加载类信息)
方法重写的条件:
1.方法名与形参列表要一样。(重载是方法名一样,形参列表不一样)
2.子类访问修饰符必须大于或等于父类的访问修饰符。
3.子类的返回值类型一定要小于等于父类的返回值类型(子类的返回类型必须和父类一样或是父类返回值类型的子类)。
4.子类重写方法抛出的异常类型必须小于或等于父类抛出的异常。(这里及后面关于类型说的的小于等于的意思都是表示 是 其子类(小于)或者就是它(等于),大于等于也是一样的道理。)
abstract关键字要注意的:
当有一个方法需要声明,但又不能明确其如何实现时,就需要用到abstract关键字修饰该方法。 1.如果一个类中有抽象方法,那么该类也必须用abstract修饰将其变成抽象类。 2.抽象类可以同时有抽象方法和非抽象方法,可以只有抽象方法或只有非抽象方法。 3.非抽象类继承抽象类时,必须把抽象类中所有的抽象方法统统实现。抽象类继承抽象类可以不把父类的抽象方法都实现。 4.抽象类不能实例化(不能创建对象),但可以通过类名调用其中静态成员,在该类内的其他非抽象方法可以调用抽象方法。 5.abstract不能与private、final、static连用 6.abstract只能修饰方法和类
关于为什么abstract不能与private、final、static连用
简单一点的说就是两个字——矛盾 有矛盾自然不能在一起,下面解释一下矛盾在哪?
与private:一父类中若有一个方法既被abstract修饰又被private修饰那么想想:被private修饰的是对外不可见的,但是被abstract修饰的话是子类要重写的,既要重写有看不见,怎么搞??? 与final:一父类中若有一个方法既被abstract修饰又被final修饰那么想想:被final修饰的方法是要求不能重写的,但是被abstract修饰的话是子类要重写的,放一起简直离大谱。 与static:一父类中若有一个方法既被abstract修饰又被static修饰那么想想:被static修饰的方法是要求不能重写的,但是被abstract修饰的话是子类要重写的
抽象类没法创建对象,那为什么有构造方法?
答案其实很简单,就是用到前面的一个知识点:子类创建对象时是会调用父类的构造方法的。
传参问题:
其实这个点跟C语言里的传值和传地址的问题类似,只要传过去的是地址则这用于接收该地址的变量就能通过这个地址对原来变量指向的内容进行修改,但是如果只是简单的传一个数据过去的话则不能达到该效果。
两个重要接口:
1.Comparable:实现了它的类可以比较大小
2.Serializable:实现了它的类可以串行化/序列化(即可以网络传输和保存到文件中)
接口要注意的:
接口的引入:首先java是单继承的一个类只能有一个直接的父类,那么在这时候是否就会出现一些问题,当一个类具备另外两个类的特征时我们该怎么办呢?在这时候我们就要引入这么一个特殊的类——接口,类是可以实现多个接口的。
需要注意的: 1.接口里的成员变量都属于常量,默认修饰符为public static final,成员变量前不需加修饰符,编译器会默认加上,且成员变量必须在声明时就初始化。可通过 接口名.成员名直接访问接口中的属性。
2.JDK7前接口中的方法都属于抽象方法,默认修饰符为public abstract,接口里的方法要求全部是抽象方法(所有的方法都不能实现),但JDK8后可以有静态方法、默认实现方法(此时必须带上方法体)。默认方法需加上default,不然则会默认为抽象方法。default和static一样可以放在访问修饰符前也可以放在访问修饰符后。
3.接口中定义抽象方法可以省略abstract关键字。
4.接口中定义的抽象方法如果不写访问修饰符,则默认为public
5.可以通过 接口名.方法名 直接访问接口中的静态方法。
6.接口默认是抽象的,跟抽象类一样不能用于创建对象的。但可以用于声明一个引用来接收该接口实现类的对象。接口也可以用于创建数组,存放其实现类的对象实例。(和类一样有多态的性质)
7.当接口声明的引用指向实现类的对象时,若要调用其实现类的特有方法,要用到向下转型。
8.接口是没有构造方法的(接口不能用于创建对象,有构造方法也没用)
9.非抽象类在实现接口时必须要实现接口中的所有抽象方法
抽象类实现接口可以实现接口方法也可以不用实现该方法。
10.接口不能继承其他类,但可以继承其他接口(注意是(extends)而不是(implements))。
11.接口的访问修饰符只能是public(啥也不写就也是默认为public)
12.如果一个类继承了一个类并且实现了一个接口,且其父类和接口有一个同名的属性,那么在本类中访问该属性必须明确是哪一个的(父类的用super.属性名,接口的用 接口名.属性名)
接口的作用: 1.拓展功能 2.定义约束规范 3.程序的解耦(程序要求:高内聚低耦合)
为什么一个类可以支持多接口而不可以多继承?
因为如果一个类支持多继承那么如果继承的几个类中若有同名的方法,在后面对象执行该方法时就不知如何选择了。而接口的方法都是抽象的,要到实现该接口的类中才实现,就不会发生这种矛盾了。
接口和接口间有啥关系呢?
接口与接口间是可以有继承关系的。
一个接口可以继承多个接口。
如果A接口继承了B接口,则在一个类实现A接口时要实现两个接口的抽象方法
多态:
多态:方法或对象具有多种形态,是OOP的第三大特征,是建立在封装和继承基础上的。
作用:可提高代码的复用性及可维护性。
多态的体现:
方法多态:方法重载及重写
对象多态:编译类型与运行类型可以不一致(编译时类型与运行时类型见下文)。
多态的前提:必须存在继承或者实现关系。
向上转型:父类引用类型变量指向了子类的对象或者是接口的引用类型指向了接口实现类的对象。
向下转型:将父类的引用强制转换成子类的引用类型并用子类的引用接收(需注意:1.只能强制转换父类的引用,不能强制转化父类的对象。2.父类转向的目标类型必须是当前父类引用指向的对象的类型),此时通常会用到instanceof判断。
多态的应用场景: 1.用作形势参数时可以接收更多类型的参数。 2.用作返回值类型时可返回更多类型的参数。
关于编译时类型和运行时类型: 1.像 Animal animal = new Pig();这种形式创建对象时,左边的是编译时类型,而右边的是运行时的类型,在编译时不会知道运行时的类型。引用的运行时类型可以通过getClass()方法查看,即这里可以通过System.out.println(animal.getClass());来看animal的运行时类型 。
2.编译类型在创建对象时就已经确定,不能改变,但运行时类型是可以改变的。如可以将animal指向其他的,animal = new Dog(); 这样就改变了运行时的类型了。
3. A instanceof B 判断的是A的运行类型是不是B类或者B的子类。如:class A {}; class B extends A {}; A a = new B(); 则 a instanceof B; 返回的是true
多态情况需要注意的: 1. 继承关系多态的情况下,父类的引用调用和子类同名的普通成员变量,使用的是父类自己的成员变量。
2.继承关系多态的情况下,父类的引用调用和子类同名的普通成员方法,使用的是子类自己的成员方法。 3.继承关系多态的情况下,父类的引用调用和子类同名的静态成员方法,使用的是父类的静态成员方法。 4. 继承关系多态的情况下,不能访问子类特有成员(包括成员变量及成员方法)。(这跟以前也提到过的java编译器只会知道有那么个变量,但不知道该变量后边赋值的有关内容,在这里Animal animal = new Pig(); 编译器并不知道animal的指向的是Pig类的对象,所以在后面若用animal.去调用Pig子类的特有方法时会在编译时报错) 5.接口实现关系多态的情况下实现,默认都是访问子类的方法。接口和实现类如果有同名静态方法,不能通过接口的引用调用该静态方法,不然会报错,外部只能通过类名来调用接口中的静态方法。
注:多态情况下如果想要访问子类成员变量或者特有成员方法,应用到上面的向下转型。
java的动态绑定机制:
1.当调用对象方法时,由于动态绑定机制,该方法会和该对象的 内存地址/运行类型 绑定。例子如下: 2.当调用对象属性时,属性无动态绑定机制,方法中使用了某属性,则直接调用里该方法近的那个属性。例子如下:
内部类:
内部类的特点:可以直接访问外部类私有属性,可以体现类与类之间的关系。
内部类分为:1.定义在外部类局部位置上的(比如在方法中):局部内部类(含类名)、匿名内部类 2.定义在外部类成员位置上:成员内部类(无static修饰)、静态内部类(有static修饰)。
局部内部类需注意:
1.局部内部类在外部类的局部位置,只能在方法或者在代码块中定义,作用域仅为其所在的代码块或方法中,其地位相当于在其所在方法或代码块中的局部变量。
2.可以直接访问外部类所有成员,包括私有的。
3.外部类与局部内部类有成员重名时,若需要访问外部类的成员,则需要通过:外部类类名.this.成员名 来访问外部类成员,否则会遵循就近原则访问内部类中的成员。
3.局部内部类不能用除final、abstract外的修饰符修饰(访问修饰符也不可以),其地位相当于一个局部变量。注意:用final修饰后则不能被继承。
4.局部内部类中的成员不可以用static修饰。
5.外部类如果要调用局部内部中的方法,只可以在局部内部类所在的方法或代码块中,创建该局部内部类的对象访问。若局部内部类是抽象的则不可以(因为抽象的类不能创建对象)。
6.外部其他类中不能直接访问局部内部类。
6.局部内部类能访问的局部变量必须有final修饰。(原因:局部内部类对象的生命周期长于局部变量,所以jvm在局部内部类访问被final修饰的局部变量时会对其进行复制)
匿名内部类需注意:
1.匿名内部类在外部类的局部位置 ,只能在方法或者在代码块中定义,且没有类名。(此处没有名字是没有自己可以直接看到的名字,但是实际上系统会给它取个名字。)
2.匿名内部类的基本语法: new 类或接口(参数列表){类体};
3.匿名内部类可以访问外部类的所有成员(包括私有的)。
4.匿名内部类中成员不可以用static修饰。
5.外部其他类不能访问匿名内部类
6.外部类与匿名内部类有成员重名时,若需要访问外部类的成员,则需要通过:外部类类名.this.成员名 来访问外部类成员,否则会遵循就近原则访问内部类中的成员。
7.匿名内部类的优点:简化书写。如果某个类只用一次,那么直接按平常那样去定义就比较麻烦,而匿名内部类就可以解决这个问题。如下代码中,就无需直接创建Pig类,且此处new Animal()这里创建的对象是是Animal子类的对象,用Animal的引用pig来接收。pig的编译类型为Animal,运行类型为匿名内部类。而这个内部类的类名可以通过getClass()方法来查看。(前文提过,查看一引用的运行时类型可以用getClass()方法。) 分配的匿名内部类类名为:创建匿名内部类的语句所在类的类名$创建匿名内部类的语句所在类中的第几个匿名内部类。
public class Test {
public static void main(String[] args) {
Animal pig = new Animal(){
@Override
public void eat() {
System.out.println("拱白菜");
}
};
pig.eat();
System.out.println("pig的运行时类型:" + pig.getClass());//class Test$1
}
}
class Animal {
public void eat() {
}
}
8.匿名内部类的条件:必须有继承或者实现关系(不然自己没有名字没法创建对象)。 通过下图现象来解释:
从上面图中可以看出,通过匿名内部类可以简化书写,且通过因为有继承而通过父类类名创建了对象。
但明明上面继承关系中父类是抽象类,抽象类是不可以创建对象的啊,那为什么呢?
这里其实是因为:其实这里看似父类创建对象,但其实并非如此,实际上在用父类创建子类对象时会给子类分配一个类名,在new Animal(){};这一句中会在系统中完成:创建一个子类再用子类创建一个对象的过程,这一过程后再返回一个子类对象。new 接口(){}; 创建对象也是一样的道理
通过上面代码以及图片中的方式我们知道了调用方法的两种方式,可以直接在匿名内部类后调用,也可以用一个父类引用接收后在用该引用调用,但此时就会有下面图片中的问题了:
那么如何解决上面的那个问题呢?一种方法就是直接在父类中直接加上这么一个抽象方法,这虽然可以解决编译器报错的问题,但却违背了原本我们想要sleep是一个子类特有方法的目的。
所以得用另一个解决方法,先执行子类的特有方法,子类特殊方法的由于返回类型改成Animal 而 return this;则可以返回这个匿名内部类的对象然后.eat();就可以再执行eat方法了。
成员内部类需注意:
1.成员内部类在外部类的成员位置
2.内部类可以直接访问外部类的成员
3.成员内部类可以任意添加修饰符,因为其地位就相当于外部类中的一个普通成员。
4.成员内部类与外部类存在同名成员时,在内部类中遵循就近原则,默认访问的是内部类的成员。若要访问外部类成员则需要:外部类名.this.访问的成员名。
5.外部其他类使用成员内部类的两种方式:①Outer outer = new Outer(); Outer.Inner inner = outer.new Inner(); 其中两句也可以合并为一句:Outer.Inner inner = new Outer().new Inner(); ②在外部类中创建一个方法,返回一个内部类对象。若此方法名为getInnerInstance,则为 Outer outer = new Outer(); Outer.Inner inner = outer.getInnerInstance();此处两句也可以合并为一句,就不写了。注意:被private修饰的成员内部类,只有通过在外部类内创建公共的方法才能访问。在外部类无法访问。
6.如果一个成员内部类定义了静态成员,那该类也必须定义为静态的类。
静态内部类就是被static修饰的成员内部类。
为什么如果成员内部类定义了静态成员,那该类也必须定义为静态的类,而非内部类的普通情况下的类含有静态成员却不用变成静态类?
这里用到前文中提到的东西:静态数据是在类加载时存在的,它并不需要此类创建对象才会存在,静态成员的访问也是不需要依靠对象存在的,既然如此,那如果成员内部类定义了一个static修饰属性而内部类又不是静态的的话,那么它就可以通过下图的方式访问(为了大家更好的理解访问的那个语句故多用图解释一下),通过访问方式我们可以看出,访问该静态成员变量需要创建一个外部类对象,那样的话就跟上面提到的相违背了。如果在内部成员类前也加上static的话就可以直接通过 外部类类名.内部类类名.此属性变量名; 来访问了而无需创建对象。
注意:语法规定静态成员内部类在其他类中创建对象的方法为 外部类类名.内部类类名 变量名 = new 外部类类名. 内部类类名();
自定义一个枚举类的方式:
class Season {
private String name;
private String desc;
private Season(String name, String desc) {
this.name = name;
this.desc = desc;
}
public static final Season SPRING = new Season("春天", "活力");
public static final Season SUMMER = new Season("夏天", "炎热");
public static final Season AUTUMN = new Season("秋天", "凉爽");
public static final Season WINTER = new Season("冬天", "寒冷");
@Override
public String toString() {
return "Season{" +
"name='" + name + '\'' +
", desc='" + desc + '\'' +
'}';
}
}
class Test {
public static void main自定义枚举(String[] args) {
System.out.println(Season.SPRING);
System.out.println(Season.SUMMER);
System.out.println(Season.AUTUMN);
System.out.println(Season.WINTER);
}
}
枚举类:
枚举类是一种特殊的类,里面只含有一组有限的特定对象。
1.枚举值默认的修饰符是 public static final
2.枚举值类型为枚举类的类型。(注意常量要大写)
3.枚举值其实就是当前枚举类的对象。
4.各枚举值用 , 间隔 放在枚举类中的第一个语句。
5.枚举类的构造方法访问修饰符要是private,可以省略访问修饰符。如果使用无参构造器创建枚举值(枚举对象),那连 实参列表和小括号 都可以省略,只写枚举常量的名字就行了。 7.枚举类的父类为java.lang.Enum,其重写了toString方法,返回的是枚举常量的名称。并且还因为它继承了java.lang.Enum,Java是单继承的,所以它就不能再继承其他的类了,但可以实现其他接口。
异常:
1.运行时异常:一般是编译器检测不出来的,此类问题很普遍,常是编程时程序员的逻辑错误导致,是本应该避免的现象,可以不做处理。如果处理可能会对程序的可读性和运行效率产生影响。
2.编译时异常:编译器能检测出来,要求必须处置的异常。
关于try-catch-finally块要注意的:
try {
//可能出现异常的代码
}/* catch (ArithmeticException e) {
//可以有多个catch块,此处注销掉的catch块可以保留
//但要注意,先出现的catch块捕获的异常类型必须小于等于后面catch块中的异常,不然后面的异
//常都会在前面catch中捕获,那后面的catch就没有意义了。
//(如此处如果把ArithmeticException与Exception两个换一下的话就会报错)
}*/ catch (Exception e) {
//如果捕获到异常时程序员的处理方式
//如果没有捕获到异常,那么此处的代码将不会执行
//如果某一句中存在异常,则该块中剩下未执行的代码将不在执行。
} finally {
//不管try块中代码是否出现异常 或者 catch中是否成功捕获异常(如果没成功捕获,则程序会挂
// 并且jvm会抛出该异常) 再或者 前面catch块中出现了return ,此处的代码都会执行。
// 如果catch中有return但finally中也有return,那么将返回finally中的,但catch中
// return后面的内容如前置或后置++等内容会执行
// 但如果catch中有return,而finally中没有return,那么将返回catch中的,就算finally中
// 的代码对catch中的返回值进行了修改,catch返回的内容依旧是未执行finally块时的。
}
//经过上面的过程,这里的代码仍会执行。
关于throws抛出异常要注意的:
1.throws抛出异常必须大于等于方法内产生的异常。
2.一个方法如果调用了一个抛出编译时异常的方法,那么必须处理。
3.throws后可以是多个异常,他们之间用 , 隔开
4.运行时异常未做处理,相当于默认throws处理。
4.子类重写父类方法时,抛出的异常要小于等于父类的。
5.注意在某方法内时抛出异常对象是用throw,而在方法声明中抛出异常类型是用throws
自定义异常类:
1.自定义异常需继承Exception或者RuntimeException,如果继承Exception则属于编译时异常,如果继承RuntimeException,则属于运行时异常。通常情况下都继承RuntimeException。
需要注意的一个小点:
关于基本数据类型设计对应的包装类:
基本数据类型设计对应包装类最大的好处:可以让基本数据类型也使用上方法。
关于自动装箱和自动拆箱(JDK5开始有的之前是手动):
自动装箱:自动把基本数据类型的数据转成相应的包装类。
自动拆箱:自动把相应的包装类转成基本数据类型的数据。
int a = 1;
//下面两句效果是一样的,都是手动装箱
Integer integer = new Integer(a);
Integer integer1 = Integer.valueOf(a);
//下面是手动拆箱
int a1 = integer1.intValue();
//下面是自动装箱
Integer integer2 = a1;//但其底层依旧是通过valueOf方法实现的,可以通过debug在此句下断点然后进去就可以看了
//下面是自动拆箱
int a2 = integer2;//其底层也依然使用的是用intValue方法实现,也可以通过debug来查看。
两道经典的面试题:
第一道:
Object obj = true ? new Integer(1) : new Double(2.0); System.out.println(obj);
答案是1.0而不是1,三元运算符要看成一个整体,里面最高精度是double 前面的 int会自动提升。
但如果用if-else来替代则结果为1,这题要特别留意。
第二道:
Integer i = new Integer(1);
Integer j = new Integer(1);
System.out.println(i == j);
Integer m = 1;
Integer n = 1;
System.out.println(m == n);
Integer x = 128;
Integer y = 128;
System.out.println(x == y);
//第一题很简单,创建了两个对象,其引用记录了各对象的地址自然为false
//在第二三题中:
//此处需注意Integer integer = 整数;
//这一句中用到了自动装箱,其底层是用来Integer.valueOf(整数)
//所有在这就应该去看一下valueOf的源码
// (注意:不需要去完全读懂源码,完全去读懂效率是不会高的,瞟一眼连蒙带猜效果好)
//观察源码发现:当在 -128~127 这一范围则不会创建对象直接返回原本在那个范围已创建好了的,
//当两个值均再-128~127且相等的时候返回的就是同一个对象了
超过这一范围则会创建对象
//但如果 == 两边有一边是基本数据类型int,一边是其包装类Integer,那么就是比较的值的大小
//知道这些后上面的答案也就不用多说了
导包时需要注意的:
导包时到如两个包中同名的类是不可以的,碰到这种情况,只能导一个,另一个如果要用需要通过包名.类名直接使用。如:一个没导包的Dog类在com.xiaoguo的包中,则创建对象时可以: com.xiaoguo.Dog dog = new com.xiaoguo.Dog();
关于导包易理解出错的:
一个常见的错误理解:导包会把那个导入包的类加载到内存中。用通配符*导入太多不需要的类的话会导致运行慢。
实际上,只有当某个类被使用时才会加载到内存中。
为验证上面是错的,可以通过在导入的类中加入静态代码块,因为静态代码块是在类加载到内存时执行一次,所以在此可利用这一点来进行验证,验证方法见下图。
导包时不建议使用通配符的原因:使的代码结构更加清晰,即使当导入多个包时很容易就能看出导入的类来自哪个包。
详解String类:
1.常用构造方法:
String s1 = new String();
String s2 = new String(String original);
String s3 = new String(char[]);
String s5 = new String(char[], int startIndex, int count);
String s6 = new String(byte b);
2.它实现了Comparable、Serializable这两个接口:可以比较大小也可以在网络传输。
3.其被final修饰,不能被继承。
4.底层有一个叫value的字符数组,是String类的一个属性,用于存放字符串内容(在这是否想到了其charAt()方法呢🤣),并且该数组被final修饰,是不可被修改的。数组是引用类型,指向一个引用类型对象的引用被final修饰则其不能再修改指的是不能更改它指向的对象,但原本指向的那个对象的内容是可以作修改的。但还要注意:一个类如果被final修饰了,那么它声明的引用指向的一个对象时,该引用是可以更改它指向另一个对象的,但在该类声明一个引用被final修饰时则不能再更改它的指向对象。如下面第一张图是不会报错的,但第二张则报错。
5.①String str = "abc";② String str1 = new String("abc");在内存中做出的动作是不同的。 第①种方式时,它是直接在方法区的字符串常量池中找,如果找到了,则直接把该找到的字符串的地址给str,如果没有找到,则在字符串常量池中创建该字符串后再把地址给str。 第二②种方式时,会在堆中创建一个String类型的对象,然后在方法区的字符串常量池中找该字符串,找到了的化,则直接把地址给给value属性,没有找到的话则创建后再给地址。
6.String str = "hello" + "abc";这里只会创建1个常量池对象而不是3个,因为底层做出了优化,看到 字符串+ 会加拼在一起整完后再创建。
7.String a = "hello"; String b = "abc";String c = a + b;在第三句中发生的过程比较复杂,但应该知道,应在此处放断点再通过debug观察其内部发生了什么,这里说一下主要步骤:①创建一个StringBuffer对象。②利用StringBuffer的append方法追加"hello" 。③利用StringBuffer的append方法追加"abc"。④再用一个toString方法返回新创建的String类型的对象。也就是说 c 其实不是直接指向常量池中的"helloabc",而是指向堆中的String类对象,而该对象的value属性指向常量池中的"helloabc"。如果还有String d = "helloabc";System.out.println(c == d); 则输出的结果为false
8.String a = "hello"; String b = a + "abc";第二局发生的的过程和上面的一样,你也可以通过debug调试来查看,但如果只是想判断会和"hello" + "abc"一样先拼接再到常量池创建该字符串,则只需String c = "helloabc";System.out.println(b == c);即可,因为如果是拼接的话返回的就是true,但实际上却是false。那么String a = "hello"; String b = "abc" + a;的话是不是先拼接再到常量池创建该字符串的了。
StringBuffer字符串缓冲区类你了解多少?
1.其父类AbstractStringBuffer中有属性char[] value,但跟String不一样的是其没有被final修饰。
2.StringBuffer是被final修饰的其不能被继承。
3.String字符串的内容一般不会随意的修改,因为每增加一次都会在字符串常量池中创建一个新的,并让原先的那个引用重新指向新的字符串,但StringBuffer中有一个字符数组作为缓冲,如果对其内容进行修改。拿增加来说,则直接用其append方法追加即可,会追加到原来数组中多出的地方,如果放满了才会创建一个新的数组并使原来的value重新指向新的数组。
4.常用构造方法:
//第一种,构造一个不带字符的字符缓冲数组,默认初始化容量为16
//StringBuffer stringBuffer = new StringBuffer();
//第二种,按给出容量大小构造一个不带字符的字符缓冲数组
//StringBuffer stringBuffer = new StringBuffer(int capacity);
//第三种,通过给一个String构造一个字符缓冲数组,在当前字符串长度的基础上再增加16个大小的容量
//也就是说这个数组的大小为:字符串长度+16
//StringBuffer stringBuffer = new StringBuffer(String str);
5.String与StringBuffer的相互转换:
String---->StringBuffer
//第一种
String str = "八戒";
StringBuffer stringBuffer = new StringBuffer(str);
//第二种
StringBuffer stringBuffer1 = new StringBuffer();
stringBuffer1.append(str);
StringBuffer---->String
//第一种:
String str1 = stringBuffer.toString();
//第二种:
String str2 = new String(stringBuffer1);
StringBuilder 一道题:
public class InterviewTest {
public static void main(String[] args) {
String s1 = "abc";
StringBuilder sb = new StringBuilder("abc");
//1.此时调用的是String类中的equals方法.
//保证参数也是字符串,否则不会比较属性值而直接返回false
//System.out.println(s1.equals(sb)); // false
//StringBuilder类中是没有重写equals方法,用的就是Object类中的.
System.out.println(sb.equals(s1)); // false
}
}
String、StringBuffer和StringBuilder的比较:
1.StringBuffer和StringBuilder非常类似,都代表可变字符序列,方法也基本一样(StringBuilder的方法没有synchronized)
2.String是不可变字符序列,效率低,但复用率高。
StringBuffer是可变字符序列,效率较高,是线程安全的。
StringBuilder是可变字符序列,效率高,是线程不安全的。
3.使用原则:如果字符串经过大量的修改,那么用StringBuffer或StringBuilder
如果字符串经过大量的修改,并且再单线程的情况下,用StringBuffer
如果字符串经过大量的修改,并且再多线程的情况下,用StringBuilder
如果字符串很少修改,被多个对象引用,则用String,如配置信息等
当碰到一个特别大的数基本数据类型都存不下时:
当是整数时用BigInteger类进行处理。当是小数时用BigDecimal类处理。
BigInteger bigInteger1 = new BigInteger("10474368434919493465319");
BigInteger bigInteger2 = new BigInteger("33749357935619");
BigInteger bigInteger = bigInteger1.divide(bigInteger2);//如果bigInteger2为0则会抛出异常
BigDecimal bigDecimal1 = new BigDecimal("134354654353453453443.214321515");
BigDecimal bigDecimal2 = new BigDecimal("11.1");
//下面第二个参数避免除出来是个无限小数而抛出异常
BigDecimal bigDecimal = bigDecimal1.divide(bigDecimal2, BigDecimal.ROUND_CEILING);
获取系统时间:
Date date = new Date();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒 E");//可在jdk api中查看,上面最有一个E表示星期几
System.out.println(dateFormat.format(date));
Calendar c = Calendar.getInstance();
System.out.println(c.get(Calendar.YEAR) + "-" + (c.get(Calendar.MONTH) + 1) + "-" + c.get(Calendar.DAY_OF_MONTH)
+ " " + c.get(Calendar.HOUR_OF_DAY) + ":" + c.get(Calendar.MINUTE) + ":" + c.get(Calendar.SECOND));
//月要注意的:(c.get(Calendar.MONTH) + 1)后面有个+1别忘了
//小时要注意的:HOUR_OF_DAY表示的是24小时如果12小时则是HOUR
LocalDateTime ldt = LocalDateTime.now();//返回当前时间对象
System.out.println(ldt.getYear() + "-" + ldt.getMonthValue() + "-" + ldt.getDayOfMonth()
+ " " + ldt.getHour() + ":" + ldt.getMinute() + ":" + ldt.getSecond());
//月份获取注意是:ldt.getMonthValue(),如果是用的getMonth则出来的是月份的英文
//上面的输出格式也可以通过DateTimeFormatter来实现,其使用类似于SimpleDateFormat
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
System.out.println(dateTimeFormatter.format(ldt));
//LocalDate类的使用跟LocalDateTime相似,但只有上面的获得年月日的方法
//LacalTime类的使用跟LocalDateTime相似,但只有上面的获得时分秒的方法
//时间戳的用法
Instant instant = Instant.now();
System.out.println(instant);//可以直接打出日期
//通过Date的from方法可以将Instant转成Date
Date date = Date.from(instant);
//通过Date的toInstant方法可以将Date转成Instant
Instant instant1 = date.toInstant();
集合:
1.集合的特点是:可以添加任意类型的元素。
2.在集合中所谓的有序不是指自然顺序,而是指添加进去的顺序与存储顺序一致。
Collection接口下集合要注意的:
1.实现了Collection的接口当增加的是基本类型的元素时,实际上增加进去是相应的包装类对象。 如:List list = new ArrayList(); list.add(1); 后面那句实际上相当于:list.add(new Integer(1));
2.所有实现了Collection接口的类都有一个iterator()方法,获取迭代器eg::Iterator iterator = list.iterator(); 使用迭代器+while循环遍历整个集合后,游标指向的是最后一个元素,此时不能再用next()方法取下一个元素,不然会抛出NoSuchElement异常。如果还需使用,则应重置该迭代器。重置迭代器eg:iterator = list.iterator();
3.增强型for循环可以用于遍历集合,在遍历集合时其底层的实现依然依靠迭代器。虽然其底层也是依靠迭代器但使用时还是有区别:增强for循环只能遍历集合元素,不能增删。而迭代器可以通过迭代器的方法进行增删。
迭代器中要注意的事项:
1.迭代器在迭代过程中不准使用集合对象的方法,如:list.add("旺财"); list.remove(0))改变集合的元素个数,否则会报出ConcurrentModificationException
2.在迭代过程中如果需要改变集合中的元素个数,只能使用迭代器的方法。(这点其实就是对上面这点的补充)
List接口要注意的:
1.实现了List接口的集合类具备的特点是:有序(和添加的顺序一样),可重复。
2.因为实现了List接口的类是有序的,所以支持索引。可通过.get(索引);来访问其中的元素,也可以通过索引来指定添加删除元素的位置等。
3.因为实现了List接口的类支持索引,所以遍历实现了List的类的对象时可以用三种方法:①迭代器②增强for循环③for循环。
4.List list = new ArrayList(); list.add("猪八戒");list.add("猪八戒");,当用list.remove("猪八戒"); 时只会删掉前面的那个,再次使用list.remove("猪八戒");时会把后面那个也删掉。
5.虽然实现了List接口的类是有序的,但判断一个集合是否包含另一个集合时只看元素是否包含,跟顺序无关:List list1 = new ArrayList(); list.add("猪八戒");list.add("孙悟空"); List list2 = new ArrayList(); list.add("孙悟空");list.add("猪八戒"); list1.containsAll(list2);返回的结果为true。因为里面它是在list1中逐个找list2中的元素,如果都能找到就是true,并不是把list2中的元素看出一个整体。 List list1 = new ArrayList(); list.add("猪八戒"); List list2 = new ArrayList(); list.add("猪八戒");list.add("猪八戒"); 所以list1.containsAll(list2);返回结果也为true。
下面就是几个常用集合了,大家注意里面的存储原理在这只是大概概括了一下,但真要全写内容实在太多,但面试时又常考,所以大家可以直接在本站中搜一下,有一些文章也是写的非常好的。
ArrayList:
1.ArrayList集合中可以加入任意元素,包括null
2.ArrayList底层是通过数组来实现数组存储的。
3.一道笔面试题:说一下ArrayList在使用无参构造方法不指定初始容量和用含参构造方法指定初始容量及容量不足时其内部是怎么处理的?
ArrayList类的底层使用了一个Object类型数组elementData去实现的,使用无参构造方法创建对象时默认初始容量为0,当第一次添加元素扩容时,扩为10,如果再次扩容,则变为原来的1.5倍。查看jdk源码就可以知道。使用含参构造方法创建对象指定容量时,初始容量为指定的那个(指定0则默认10,指定负数抛出异常),后面容量不够时则增加为原来的1.5倍。
3.ArrayList基本等同于Vector ,但是ArrayList是线程非安全的(执行效率高),还有就是扩容的时候,Vector使用无参构造方法创建对象时默认初始容量为10,满了之后变为原来的两倍。使用含参构造方法创建对象指定容量时,如果指定的数>0,初始容量为指定的那个,后面容量不够时则增加为原来的2倍,如果指定的小于0则抛出异常,但如果指定的是0,后面不够时先看第一次添加了多少个,如果是add方法添加一个,则扩为1,如果是addAll方法添加几个,则是扩容为添加的个数,后面再添加时则是变为原来的两倍。(当然其还有一个构造方法可以指定capacityIncrement,指定后每次增量都为指定的数)此处应该要自己去看源码,关键还是要能看懂源码。
LinkedList:
1.底层实现了双向链表,增删效率高。
2.LinkedList集合中可以加入任意元素,包括null
3.线程不安全。
4.其remove方法可以不指定索引删除(ArrayList和Vector都需要),不指定时默认删第一个。
ArrayList与LinkedList的比较:
ArrayList底层是可变数组,LinkedList底层是双向链表。相比于LinkedList来说ArrayList的改查效率高,增删效率低,而LinkedList改查效率低,增删效率高。
选择:改查多用ArrayList,增删多用LinkedList
Set接口要注意:
1.实现了Set接口的集合类的特点:无序(指和添加的顺序不一样),不可重复
2.因为实现了Set接口的类是无序的,所以不支持索引。
3.因为实现了Set接口的类不支持索引,所以Set接口的类的对象时可以用两种方法:①迭代器②增强for循环
HashSet:
1.HashSet的底层是HashMap,而HashMap的底层是:数组 + 链表 + 红黑树(二叉树)
2.添加的元素可以是null
3.HashSet的存储原理:①往HashSet中添加元素的时候,首先会元素的hashCode方法处理后得到元素的哈希值(hashCode与哈希值并不等同,哈希值是hashCode结果经过处理后的),通过元素的哈希值就可以得到索引值然后找到存储数据表table中的对应的位置。②如果算出的位置没有其他元素,则可以直接存储到该位置。③但如果算出的元素目前已有其他元素在该位置,那么还会用该元素的equals方法与该位置上的元素进行比较,如果符合可以存的条件,那么该元素可以存储到该位置的最后面,如果返回值是true,则会被视为重复元素,不允许存储。
4.HashSet扩容以及转成红黑树的机制:扩容机制:第一次添加元素时,底层的table数组扩容为16,临界值(threshold)是16*加载因子(loadFactor=0.75) 结果为12,如果集合中已经存了12个元素(注意:这里的12个元素不是值table数组中存了12个,而是针对整个集合存了12个),那么在存13个元素时,那么会扩容为16*2,新的临界值为32*0.75 = 24,以此类推。 转成红黑树的机制:在Java8中如果一条链表的结构 >= TREEIFY_THRESHOLD(默认为8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认64),则会进行树化,但如果只满足其中一个条件的话,则会再对数组进行扩容。
4.下面一道经典的题:
public class Test {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
boolean b1 = hashSet.add(new Dog());
boolean b2 = hashSet.add(new Dog());
boolean b3 = hashSet.add("悟空");
boolean b4 = hashSet.add("悟空");
boolean b5 = hashSet.add(new String("八戒"));
boolean b6 = hashSet.add(new String("八戒"));//因为String类型的equals方法比较的是字符串的内容,所以这里尽管创建了一个新的对象也不可以再存进去。
System.out.println(b1);//T
System.out.println(b2);//T
System.out.println(b3);//T
System.out.println(b4);//F
System.out.println(b5);//T
System.out.println(b6);//F
}
}
class Dog {
}
LinkedHashSet:
1.LinkedHashSet是HashSet的子类。添加的元素和HashSet一样可以是null。其底层是一个LinkedHashMap,而LinkedHashMap底层维护了:数组+双向链表
2.其内部链表维护了元素的次序,这使得元素看起来是以加入顺序保存的。
3.其添加元素到Table的过程和HashSet是一样的,但前后相连添加的节点会通before,after属性形成双向链表,这样的话在遍历LinkedHashSet集合时就会按加入的顺序遍历,但其实内部在Table中存储的不是按顺序的。
TreeSet:
TreeSet:底层使用了TreeMap,其底层是红黑树数据结构
1.添加的元素不可以是null。
2.如果往TreeSet添加元素的时候,如果元素具备自然顺序的特性,那么TreeSet就会按照元素自然顺序进行排序存储。
3.如果往TreeSet添加元素的时候,如果元素本身不具备自然顺序的特性,那么元素所属类就要实现Comparable接口,并把元素的比较规则定义在compareTo方法中才能排序。(其实上面说的就是来自那一元素所属类实现了Comparable接口,根据生活中常见的顺序写了compare方法。如:2,1,3……存进去会按自然顺序1,2,3……其实是因为Integer类实现了Comparable接口)
但若偏不去实现Comparable接口,那么就必须得在创建TreeSet对象的时候就必须传入一个比较器对象。如果两者都没有在添加数据的时候就会抛出异常。
如果元素已经实现了Comparable接口,当创建TreeSet对象的时候又传入了比较器对象,那么比较规则优先使用比较器的。
4.如果新添加的元素与之前添加的元素在按设定的比较内容比较后返回的是0,则该元素被视为重复元素不能被添加进去。
推荐使用:比较器,因为其优先级别高且复用性高。
一道常见面试题:
HashSet和TreeSet分别是如何去重的?
①HashSet的去重机制:hashCode()+equals,底层先存入对象,然后经过运算得到应该hash值,通过hash值得到对应的索引,如果发现table索引所在位置没有数据,则放入,如果发现有,那么就会通过equals方法遍历比较,如果比较后不同则加入,如果相同则不加入。
②TreeSet去重机制:如果传入了比较器,就根据比较器实现的compare方法去重,如果方法返回的是0,则认为是相同的数据,就不添加如果没有传入比较器,则根据对象实现的Comparable接口的compareTo方法去重,如果方法返回的是0,则认为是相同的数据,就不添加。
Map接口要注意的:
1.Map接口实现类集合是用于保存具有映射关系的数据:Key-Value(双列元素)
2.key是不允许重复的,当后面存入一对元素时若其key是已经有了的,那么会将新添加的value替换原来的旧的。 value是可以重复的,不同的key可以有相同的value。
3.可以get方法传入相应的key来得到value,但不可以通过value得到key,也是一样的道理,value可以重复,而key不可以,通过value可能会有不同的key所以不可以。
4.Map接口实现类集合的遍历方法:这里相对于Collection接口的实现类来说麻烦一些,就写一下了
@SuppressWarnings({"all"})
public class Test {
public static void main(String[] args) {
Map map = new HashMap();
map.put("西游记", "孙悟空");
map.put("三国", "赵云");
map.put("红楼梦", "贾宝玉");
//①要得到所有key和value,先拿到所有key,再通过增强for循环或者迭代器。
Set keySet = map.keySet();
//增强型for循环
for (Object key : keySet) {
System.out.println(key + "-" + map.get(key));
}
//通过迭代器
Iterator iterator = keySet.iterator();
while (iterator.hasNext()) {
Object key = iterator.next();
System.out.println(key + "-" + map.get(key));
}
//②只需得到所有的value,通过values方法,然后通过 增强for循环 或者 迭代器即可。
Collection values = map.values();
//跟上面差不多,就不再啰嗦了
//③要得到所有key和value,通过entrySet方法
Set entrySet = map.entrySet();
//增强for循环
for (Object entry : entrySet) {
Map.Entry m = (Map.Entry) entry;
System.out.println(m.getKey() + "-" + m.getValue());
}
//迭代器
Iterator iterator1 = entrySet.iterator();
while (iterator1.hasNext()) {
Object entry = iterator1.next();
Map.Entry m = (Map.Entry) entry;
System.out.println(m.getKey() + "-" + m.getValue());
}
}
}
HashMap:
1.其中存的key和value可以是任意类型的,包括null。这样的一对元素会封装成HashMap.Node类型(这里HashMap.Node表明Node是HashMap的应该静态内部类)。
2.key和value都可以是null,但只可以有一个key是null(因为key不可重复)
3.HashSet的存储原理:①往HashSet中添加元素的时候,,会拿到该元素的key,首先会key的hashCode方法处理后得到key的哈希值(hashCode()得到的与哈希值并不等同,哈希值是hashCode结果经过处理后的),通过key的哈希值就可以得到索引值然后找到存储数据表table中的对应的位置。②如果算出的位置没有其他数据,则可以直接存储到该位置。③但如果算出的key目前已有其他数据在该位置,那么还会用该元素的equals方法与该位置上的元素进行比较,如果符合可以存的条件,那么该元素可以存储到该位置的最后面,如果返回值是true,则对value进行替换。
上面只是简单的概括一下,其底层源码非常重要,会在我的另一篇文章中。
HashTable:
1.HashTable的key和value都不能为null,否则会抛出NullPointerException。
2.HashTable是线程安全的,HashMap是线程不安全的。
3.其内部table数组的初始化容量为11。
3.HashTable与HashMap的使用方法是类似。
Properties:
1.其继承了HashTable,特点与HashTable类似。
2.还可用于从×××.properties文件中读取Properties类的实例对象并进行读取和修改。
TreeMap:
TreeMap:底层使用了红黑树数据结构实现。
1.key不可以为null。
2.如果往TreeMap添加元素的时候,如果元素的键具备自然顺序的特性,那么TreeMap就会按照元素自然顺序进行排序存储。
3.如果往TreeMap添加元素的时候,如果元素的键本身不具备自然顺序的特性,那么元素所属类就要实现Comparable接口,并把元素的比较规则定义在compareTo方法中才能排序。
但若偏不去实现Comparable接口,那么就得在创建TreeMap对象的时候就必须传入一个比较器对象。如果两者都没有在添加数据的时候就会抛出异常。
如果元素已经实现了Comparable接口,当创建TreeMap对象的时候又传入了比较器对象,那么比较规则优先使用比较器的。
4.如果新添加的元素与之前添加的元素在按设定的 key的比较内容比较后返回的是0,则进行value的替换。
推荐使用:比较器,因为其优先级别高且复用性高。
使用集合的选择:
单列数据:单列集合(Collection接口下集合)
允许重复:List接口
增删多:LinkedList 底层是:双向链表
改查多:Array List 底层是:可变数组
不运行重复:Set接口
无序:HashSet 底层是:HashMap
有序:TreeSet 底层是:红黑树
取出与插入顺序一致:LinkedHashSet 底层是:数组+双向链表
双列数据:双列集合(Map接口下集合)
键无序:HashMap 底层是:数组 + 链表 + 红黑树
键有序:TreeMap
键插入顺序和取出顺序一致:LinkedHashMap 底层是:HashMap
读取文件:Properties
泛型:
泛型的作用与好处:
1.编译时检查元素类型,提高安全性。
2.避免了取出数据时无谓的强制类型转换(因为存入时不加泛型的话是用Object类型接收的,取出时也是Object类) 3.通过一个占位符表示类中的某个属性的类型,返回值的类型或参数类型等,通过这个符号就可以使得类型更加灵活(至于为什么灵活,可以根据以下代码来理解)。
public class Test {
public static void main(String[] args) {
Person<String> person = new Person<String>("八戒");
System.out.println(person.getS());
Person<Integer> person1 = new Person<Integer>(10);
System.out.println(person1.getS());
}
}
//在定义Person对象时指定的类型相当于会代替此处中所有的E
class Person<E> {
E s;
public Person(E s) {
this.s = s;
}
public E getS() {
return s;
}
}
使用泛型需要注意的:
1.泛型中是不能使用基本数据类型的,如果要使用基本数据类型,那么必须使用该基本数据类型的包装类。
2.一般在写时会省略右边的不写,如:ArrayList<String> arrayList = new ArrayList<>();
3.两边的数据类型如果都写那必须一样,可以只写一边(一般写左边),如果两边啥也不写,则相当于是Object,如:ArrayList arrayList = new ArrayList();就等同于ArrayList<Object> arrayList = new ArrayList<>();
4.在指定泛型的具体类型后,传入的内容可以是指定类型及指定类型的子类(多态),如:ArrayList<Object> arrayList = new ArrayList<>(); arrayList.add("八戒");也行 5.自定义泛型的的标识符可以自定义,只要符合标识符命名规范即可,但一般自定义泛型的标识符都是使用一个大写字母,常用 T(type) E(element)
自定义泛型:相当于一个数据类型变量或者是一个数据类型占位符。
自定义泛型函数要注意的:
1.格式:修饰符 <标识符,……可以有多个> 返回类型(可以是此标识符) 方法名(参数列表,参数列表的参数可以使用此处声明的泛型) {}
2.自定义函数声明的泛型可在函数的:返回类型,参数列表,方法体中使用。
3.函数上自定义泛型的数据类型,是在调用该函数的时候传递实参数据的时候确定数据类型的。 如:public<T> void eat(T t) {} 传过来的t是什么类型那这里的T就是什么类型。
自定义泛型类要注意的:
1.类上自定义泛型 class 类名<标识符,……可以多个> {其中可以有多个泛型成员}
2.普遍成员(包括普通的成员属性和成员方法)可以使用泛型。
3.静态属性不能使用泛型。因为泛型类上的泛型要创建对象时才给,而静态的东西是只要类加载就会加载,可以直接用类调用而不需创建对象。
4.静态的方法不能使用类上声明的自定义泛型,如果需要使用自定义泛型,则只能在自己方法上声明。且自己声明的自定义泛型类可以与类上声明的泛型类标识符相同,因为此处的是局部的,并不与其他地方的冲突。
5.使用泛型的数组不能初始化,如:T[] arr = new T[10];这样是不可以的,但可以声明,如:T[] arr; 。 因为不知道数据类型不知道要开辟多大空间。
6.在创建对象时要指定泛型的具体类型,否则默认为Object。
自定义泛型接口要注意:
1.格式 interface 接口名<标识符,……可以多个> {}
2.接口中成员属性都是静态的,其成员属性不可以使用泛型。
3. 在其中的抽象方法和默认方法中可以使用泛型,但在静态方法中不可以。关于接口中可以有的几种方法忘了的可以Ctrl+F 搜:接口要注意的
4.接口上自定义泛型的具体数据类型要在类实现该接口或接口继承该接口时确定,如:interface A<T> {} class B implements A<此处指定T的具体类型> {} 倘若在实现时没有指定自定义泛型的具体数据类型,那么默认为Object类。
5.如果想要在接口实现类创建对象时再指定自定义泛型的具体数据类型,则应该 class Demo<T> implements A<T> {}
<?> 表示支持任意泛型类型。
泛型的上下限:
<? super A> 泛型的下限 只能用于A或者是A的父类具体类型。
<? extends A> 泛型的上限 只能用于A或者是A的子类具体类型
关于可变参数:
可变参数 数据类型... 变量名(如:public static void add(int... arr){}) 可以接受 0~多个 实参
可变参数需注意:
1.如果一个函数的形参使用了可变参数,那么调用该函数传递参数的时候可以传任意个参数。
2.当传递数据时,jvm会自动把实参数据保存到一个数组中,再传递给可变参数。
3.可变参数可以和普通类型的参数一起放在形参列表中,但可变参数必须位于形参列表的最后面(因此最多只能有一个可变参数)。 4.可以通过 变量名.length 来看传入实参的个数。 5.可变参数用起来跟数组差不多,可以通过 变量名[索引值] 访问传入的实参。 实例如下:
public static void main(String[] args) {
int sum = add(1, 2, 3, 4, 5);
System.out.println(sum);
}
public static int add(int... arr) {
int sum = 0;
for(int i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum;
}
多线程基础:
几个基本概念:
1.进程:运行的程序都叫进程,操作系统会未进程分配空间。
2.线程:线程是由进程创建(线程也可以创建线程),且一个进程可以有多个线程。线程是进程中的一个代码执行路径,负责代码的执行。
3.多线程 : 在一个进程中有多个线程在执行不同的任务代码。
4.并发:同一时间段(极短),多个任务交替执行,交替速度快,看起来同时执行一样。如单核CPU执行多个任务
5.并行:同一时刻,多个任务同时执行。如:多核CPU同一时间各自干自己的事情
注意:
只有在多CPU的情况中,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的。
并发的多个任务之间是互相抢占资源的
并行的多个任务之间是不互相抢占资源的
多线程的优缺点:
多线程的好处:
1. 解决在一个进程中可以同时执行多个任务代码的问题。
2. 提高了资源利用率。未提高效率。
多线程的缺点:
1. 增加了cpu的负担。
2. 降低了一个进程中线程的执行概率.
3. 引发了线程安全问题。
4. 引发了死锁现象.
如何创建线程:
1. 自定义一个类继承Thread。
2. 重写Thread类的run方法, 把自定义线程的任务代码定义在run方法上。
疑问: 重写run方法的目的是什么?
每个线程都有自己的任务代码, main线程的任务代码是main方法里面的所有代码, 而自定义线程的任务代码就是run方法中的所有代码。
3. 创建自定义线程对象。
4. 调用线程的start方法开启线程, 一个线程一旦开启就会执行run方法中的所有代码。
5.在一个线程中启动另一个线程,不会等到启动的线程结束后才继续执行启动线程语句后面代码。
6.主线程结束了,在主线程中启动的其他线程并不会结束。各线程的生命周期是相互独立的。
注意: run方法千万不能直接调用,直接调用run方法相当于调用了一个普通的方法则会等到该run方法的内容全部执行完才会执行后面的语句,并没有开启一个新的线程。
真正启动线程的是start方法中的start0方法,从而使线程进入可运行状态。关于线程的状态,请看下面的线程生命周期图。
另一种创建线程的方式:
1.因为Java是单继承的,当一个类需要继承其他类时就没法继承Thread类了,所以这里需要用到另一种创建线程的方法——实现Runnable接口
2.Runnable接口并没有start方法,此时需要创建一个线程来调用start方法,操作如下:
3.并且很多时候更建议使用此方法去创建线程,因为此方法创建的线程可以实现资源的共享并且还不需要担担心单继承机制的影响。如何实现资源的共享也同样看下操作:
public class Test {
public static void main(String[] args) {
Dog dog = new Dog();
Thread thread1 = new Thread(dog);
thread1.start();
//可创建多个Thread对象,传入dog对象,而使得多个线程共享一个dog资源
Thread thread2 = new Thread(dog);
thread2.start();
}
}
class Dog implements Runnable {
@Override
public void run() {
System.out.println("Dog线程");
}
}
几个常见的问题:
①:Runnable实现类对象是线程对象吗?
runnable实现类的对象并不是一个线程对象,只不过是实现了Runnable接口的对象而已。
只有Thread的实例对象或者Thread的子类对象才是线程对象。
②:为什么要把Runnable实现类的对象作为参数传递给thread对象呢?作用是什么?
因为通过Thread类对象的start方法才能启动一个线程。作用:是把Runnable实现类的对象的run方法作为了任务代码去执行了。
③:一个java应用程序在运行的时候至少有几个线程?
2个线程, 主线程, 垃圾回收器线程。
④:线程安全问题出现的根本原因?
1. 存在着两个或者两个以上的线程。
2. 多个线程共享了着一个资源, 而且操作资源的代码有多句。
线程常用方法:
Thread.sleep(毫秒数); 或 线程对象名.sleep(); 休眠指定毫秒数。当用Thread.sleep(毫秒数);时在哪个线程中调用则哪个休眠
线程对象名.interrupt();方法可打断该休眠。
currentThread() 返回当前执行该方法的线程对象引用。(在哪个线程的任务代码中调用则返回哪个线程对象的引用)
可在线程执行的代码中通过Thread.currentThread().getName(); 或者 线程对象名.setName(String name); 来设置线程名。
可Thread.currentThread().setName(String name); 或者 线程对象名.setName(String name); 来设置线程名。
线程对象名.setPriority(Thread. 优先级); 设置线程优先级。
线程对象名.getPriority(); 获取线程优先级。
线程对象名.yield();使该线程对象的礼让,让出CPU使其他线程执行,但礼让时间不确定,所以不一定能礼让成功。
线程对象名.join();线程插队。当a线程中调用了 b.join();时,CPU会被从a线程让给b线程,a线程中b.join();后的代码暂时不会执行,等b线程中的任务代码全部执行完后才会继续执行。也可以在b.join(time);方法中传入指定时间,则在该时间内CPU会被从a线程让给b线程。
线程终止:
1.在线程完成任务时会自动终止。
2.使用 通知方式 ,通过变量来控制run方法退出从而终止线程。操作如下:
public class Test {
public static void main(String[] args) throws InterruptedException {
Dog dog = new Dog();
dog.start();
Thread.sleep(5000);
dog.setLoop(false);
System.out.println("主线程结束了");
}
}
class Dog extends Thread {
private boolean loop = true;
@Override
public void run() {
while (loop) {
System.out.println("狗");
}
}
public void setLoop(boolean loop) {
this.loop = loop;
}
}
守护线程:
Java中线程分两种:用户线程和守护线程。
1.用户线程:一般也叫工作线程,平时用到的普通线程均是用户线程,当在Java程序中创建一个线程,它就被称为用户线程。
2.守护线程:为用户线程服务。常见的守护线程:垃圾回收机制。
3.main线程结束后守护线程就结束,所以当一个Java程序只剩下守护线程时,守护线程会立马结束。如果需要将子线程设置为守护线程,则通过:线程对象名.setDaemon(true); 即可设置为守护线程,但要注意,设置为守护线程的语句一定要先于启动线程的语句。
线程同步机制:
使当有一个线程对资源操作时,其他线程不可以对这个线程操作,只有当该线程完成的时候其他线程才可以该资源进行操作。虽然同步机制可以解决线程安全问题但是会使得代码的执行效率降低。
同步的具体实现方法:
1.使用同步代码块:
synchronized (对象) {
//只有获得锁对象才能执行此处同步代码块的内容
}
2.在方法声明中加上synchronized关键字,使整个方法为同步方法。如:
public synchronized void eat() {} //这时锁为this对象。
互斥锁:
Java中引入互斥锁的概念是为了保证共享数据的操作的完整性。
每个对象都对应于一个可称为互斥锁的标记,而这个标记就用于保证在某一时刻只能有一个线程访问该对象。
通过关键字synchronized来与对象的互斥锁联系。当某个对象被synchronized修饰时,该对象在某一时刻只能被一个线程访问。
同步代码块要注意的细节:
1. 锁对象可以是任意的对象、.
2. 锁对象必须是多个线程共享的对象(锁对象必须是唯一,要同一把锁才行)。
3. 线程调用了sleep方法是不会释放锁对象的。调用wait方法会释放锁对象。
4. 只有会出现线程安全问题的时候才使用java的同步机制(同步代码块和同步方法)
同步方法要注意的事项:
1. 非静态同步方法的锁对象默认是this对象,静态方法的锁对象是当前所属类的class文件对象。也就是: 当前类类名.class
2. 同步方法的锁对象是固定的,无法更改。
3.普遍的方法中使用同步代码块时其锁对象符合上面说到的,但是如果是静态方法中使用同步代码块,那么该锁对象就必须是:当前类类名.class 关于此点,用下面例子来加深大家理解:
public class Test {
public static void main(String[] args) throws InterruptedException {
SellTickets sellTickets = new SellTickets();
Thread thread1 = new Thread(sellTickets);
Thread thread2 = new Thread(sellTickets);
Thread thread3 = new Thread(sellTickets);
thread1.setName("窗口1");
thread2.setName("窗口2");
thread3.setName("窗口3");
thread1.start();
thread2.start();
thread3.start();
}
}
class SellTickets implements Runnable {
private static int num = 100;
private static boolean loop = true;
@Override
public void run() {
while (loop) {
sell();
}
}
public static void sell() {
//此处的锁对象必须为SellTickets.class,否则编译报错
synchronized (SellTickets.class) {
if (num <= 0) {
System.out.println(Thread.currentThread().getName() + "卖完了");
loop = false;
return;
}
System.out.println(Thread.currentThread().getName() + "卖出了第" + (num--) + "号票");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
推荐使用: 同步代码块
推荐的原因:
1. 同步代码块的锁对象可以由我们自己指定,同步方法的锁对象是固定的。
2. 同步代码块可以随意指定那个范围需要被同步,而同步方法必须是整个方法都同步, 代码不灵活。并且这样可以使同步的代码尽量的少,从而提高效率。
线程的死锁问题:
死锁现象出现的根本原因:
1. 存在两个或者两个以上的线程存在。
2. 多个线程必须共享两个或者两个以上的资源。
死锁现象一个经典例子如下:
public class Test {
public static void main(String[] args) throws InterruptedException {
DeadLocked d1 = new DeadLocked(true);
DeadLocked d2 = new DeadLocked(false);
d1.start();
d2.start();
}
}
class DeadLocked extends Thread {
//两个线程同时共享o1,o2
static Object o1 = new Object();
static Object o2 = new Object();
boolean flag;
public DeadLocked(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
synchronized (o1) {
System.out.println(Thread.currentThread().getName() + "进入if");
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + "进入if");
}
}
} else {
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + "进入else");
synchronized (o1) {
System.out.println(Thread.currentThread().getName() + "进入else");
}
}
}
}
}
java同步机制解决了线程安全问题,但是同时也引发了死锁现象。
死锁现象如何解决呢? 没法解决。 只能写代码时尽量去避免死锁现象。
关于锁的释放:
下面是会释放锁的操作:
1.当前线程在同步代码块、同步方法执行完毕后锁会释放。
2.当前线程在同步代码块,同步方法中遇到break、return语句时会释放。
3.当前线程在同步代码块,同步方法中出现了未处理的Error或者Exception导致异常结束。
4.当前线程在同步代码块,同步方法中执行了线程对象的wait()方法,当前线程暂停并释放锁。
下面是不会释放锁的操作:
1.当前线程在同步代码块,同步方法中执行了线程对象的sleep()、yield()方法暂停当前线程的执行,但不会释放锁。
2.当前线程在同步代码块时,其他线程调用了该线程的suspend()方法将该线程先挂起,此时不会释放锁。
(注意suspend()、resume()方法现在已经过时不在使用了)
IO流(这一部分很少使用细节,关键是会用):
IO流技术:解决设备与设备间数据传输问题。如:内存——>硬盘、硬盘——>内存、键盘——>内存等。
文件:保存数据的地方。
创建文件的方式:
public static void main(String[] args) throws IOException {
//创建文件的方式
//① new File(String pathname)
String path = "G:\\a.txt";
//此步只会在内存中创建这么一个文件对象,未与硬盘发生关系
File file = new File(path);
//此步会真正在硬盘中创建文件
file.createNewFile();
//② new File(File parent, String childPath)
File file1 = new File("g:\\");
String child = "a.txt";
File file2 = new File(file1, child);
file2.createNewFile();
//③ new File(String parent, String child)
String parent = "g:\\";
String child1 = "a.txt";
File file3 = new File(parent, child1);
file3.createNewFile();
}
关于目录分隔符:
在Windows的目录分隔符: 可以是:'\' 反斜杠 或 '/' 正斜杠
在Unix/Linux这些操作系统中为:'/'正斜杠
在Java中'\'有着转义的特殊含义,所以在Java中如果要表示'\'则需要用到两个,如:'\\',还有上面的代码用的也是反斜杠所以都用了两根,但如果用正斜杠的话只用一根即可
Java是跨平台的,但如果代码在Windows系统中写了两个反斜杆,如上面的代码,那么拿去Linux操作系统就不能用了,所以官方给出了一种解决方法如下:(但解决方法有点不方便,所以大多数情况还是用'/'保证它尽可能多的跨平台)
File file = new File("G:"+File.separator+"a.txt");//用File.separator代替目录分割符
//一般喜欢像下面这样用正斜杠
File file1 = new File("G:/a.txt");
关于绝对路径及相对路径:
绝对路径:文件的完整路径(以盘符开头),如:G:/a.txt
相对路径:资源文件相对于现在路径的路径。如果资源文件与当前路径不在同一个盘中,那么没法写相对路径。看下面代码理解相对路径:
// .代表当前所在路径
// ..代表上级路径
File file = new File(".");
System.out.println(file.getAbsolutePath());//获取当前位置的绝对路径G:\idea_java_projects\test\.
//那么相对于当前所在的路径我在G盘根目录下的a.txt文件的路径应该就是../../a.txt 注意此处/也可以用\\
File file1 = new File("../../a.txt");
System.out.println(file.exists());//结果为true
IO流分类:
1.按操作数据单位分为:字节流(8 bit)和字符流(按字符读) 基类:(字节流)InputStream OutputStream (字符流)Reader Writer
2.按流向分为:输入流和输出流
3.按流的角色分为:节点流、处理流/包装流
节点流:可以从一个特定的数据源读取数据,如FileInputStream、FileOutputStream、FileReader、FileWriter等
处理流/包装流:“连接”在已存在的流(节点流/处理流)之上,为程序提供更为强大的读写能力,也更加灵活,
如:BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter、ObjectOutputStream、ObjectInputStream等
序列化与反序列化:
序列化:在保存数据时,保存数据的值和数据类型。
反序列化:恢复数据时,恢复数据的值和类型。
要注意的:
当需要让某个对象可序列化时,需要让其类可序列化,为了让某个类可序列化,该类必须实现以下两接口之一:Serializable、Externalizable
在序列化对象时,默认将里面的属性都序列化,除了static和transient修饰的成员
在序列化对象时,要求里面属性的类型也实现序列化接口,否则会报错
某类如果进行了序列化,则其子类也默认会序列化
最好添加序列化版本号,提高兼容性,当添加序列化版本号时,该类如果添加其它与原来已有类的新元素则该类仍不会认为是新类,只是看作原来那个的修改版或升级版。
标准输入输出流:System.in System.out
System.in 的编译类型为:InputStream 运行类型:BufferedInputStream 默认设备:键盘
System.out 的编译类型:PrintStream 运行类型:PrintStream 默认设备:显示器
含图片、音频、视频的文件使用字节流
只含文字的用字符流
对象流:ObjectInputStream ObjectOutputStream
作用:将对象进行序列化及反序列化
序列化:在保存数据时,保存数据的值和数据类型。
反序列化:恢复数据时,恢复数据的值和类型。
要注意的:
当需要让某个对象可序列化时,需要让其类可序列化,为了让某个类可序列化,该类必须实现以下两接口之一:Serializable、Externalizable
在序列化对象时,默认将里面的属性都序列化,除了static和transient修饰的成员
在序列化对象时,要求里面属性的类型也实现序列化接口,否则会报错
某类如果进行了序列化,则其子类也默认会序列化
最好添加序列化版本号,提高兼容性,当添加序列化版本号时,该类如果添加其它与原来已有类的新元素则该类仍不会认为是新类,只是看作原来那个的修改版或升级版。
标准输入输出流:System.in System.out
System.in 的编译类型为:InputStream 运行类型:BufferedInputStream 默认设备:键盘
System.out 的编译类型:PrintStream 运行类型:PrintStream 默认设备:显示器
转换流:
字节流->字符流:new InputStreamReader(InputStream, Charset/String/) new OutputStreamWriter(OutputStream, Charset/String/) 其中Charset是指编码方式
打印流:只有输出流没有输入流
PrintStream
PrintWriter
Properties类
作用:专门用于读写配置文件的集合类:配置文件的集合类:键=值
常用方法:
load:加载配置文件的键值对到Properties对象
list:将数据显示到指定设备
getProperty(key):根据键获取值
setProperty(key, value)设置键值对到Properties对象
store:将Properties中的键值对存储到配置文件,在idea中,保存信息到配置文件,如果含有中文,会存储为unicode码
网络编程:
网络通信:两台设备之间通过网络实现数据传输
java.net包下提供了一系列类和接口,供程序员使用,完成网络通讯
网络:两台或多台设备通过一定的物理设备连接起来构成网络。
根据网络的覆盖范围不同,对网络进行分类:
局域网:覆盖范围小,仅仅覆盖一个单位,如学校或公司等
城域网:覆盖范围较大,可以覆盖一个城市
广域网:覆盖范围最大,可以覆盖全国甚至全球。万维网是广域网的代表
ip地址:标识主机的编号
ip地址的组成:网络地址+主机地址,如:192.168.16.69
IPV4:由4个字节32位表示,一个字节的范围是0~255
IPV4最大的问题在于:网络地址资源有限,严重制约了互联网的发展和应用。IPV6的使用不仅能解决网络地址资源数量问题,而且也解决了多种设备连入互联网的障碍。
IPV6:是互联网工程任务组设计的用于替代IPV4的下一代IP协议,其地址数非常多。
域名:域名代表了ip,是由ip地址映射而来,可以解决记ip的困难
端口:用于区分不同的服务 表示形式:整数 范围:0~65535
在网络开发中不要使用0~1024的端口
常见网络程序端口号:tomcat:8080、mysql:3306、orcle:1521、sqlserver:1433
访问网站、邮件等各种服务时需要ip+端口
因为ip是网络上主机的编号,所以可以通过ip可找到服务的主机,再通过端口可以找到该主机的相应的服务
网络通信协议:
tcp/ip是网络通讯协议,这个协议是网络的最基本协议。
TCP/IP模型:应用层、传输层(TCP)、网络层(IP)、物理+数据链路层
TCP协议:传输控制协议
1.使用TCP协议前,须建立TCP连接,形成数据传输通道
2.传输前,采用“三次握手”的方式,是可靠的。
3.TCP协议进行通信的两个应用程序:客户端、服务端
4.在连接中可进行大量的数据传输
5.传输完毕还需释放已建立的连接,效率较低。
TCP网络通信一个小知识:
当客户端连接到服务端后,实际上客户端也是通过一个端口和服务端进行通讯的,这个端口是TCP/IP来分配的,是随机的
UDP协议:
1.将数据、源、目的 封装成数据包,不需要建立连接
2.每个数据包的大小限制在64k内,不适合传输大量数据。
3.因无需连接,故是不可靠的
4.发送数据结束时无需释放资源(因为不是面向连接的),速度快
5.没有像TCP那样的客户端和服务端,演变为发送端及接收端
InetAddress类常用方法:
1.getLocalHost 获取本机的InetAddress对象---也就是ip地址对象
2.getByName 根据指定主机名/域名获取ip地址对象
3.getHostName 获取InetAddress对象的主机名
4.getHostAddress 获取InetAddress对象的地址
Socket类:实现了TCP协议网络通讯
1.套接字(Socket)开发网络应用程序被广泛采用,以至于成为事实上的标准
2.通信的两端都要有Socket,是两台设备间通信的端点
3.网络通信其实就是Socket之间的通信
4.Socket允许程序把网络连接当成一个流,数据在两个Socket之间通过IO传输
5.一般主动发起通信的应用程序属于客户端,等待通信请求的为服务端
DatagramSocket和DatagramPacket(数据包):实现了基于UDP协议网络程序
1.UDP数据包通过数据包套接字DatagramSocket 发送和接收,系统不保证UDP数据包一定能安全送到目的地
也不能确定什么时候能到达。
2.将数据封装到DatagramPacket对象,变成一个UDP数据包,在数据包中包含了发送端的IP地址和端口号
以及接收端的IP地址和端口号。当接收端接受到DatagramPacket对象时需要进行拆包
3.UDP协议中每个地址都给出了完整的地址信息,因此无需建立发送方和接收方的连接
netstat指令:(在控制台使用)
1.netstat -an 可以查看当前主机网络情况,包括端口监听情况和网络连接情况
2.netstat -an | more 可以分页显示 出现光标后点击空格即可看下一页
反射
反射是框架的灵魂。
框架:半成品软件。可以在框架的基础上进行软件开发,简化编码
反射:将类的各个组成部分封装为其他对象,这就是反射机制 (如下图可以看出:通过类加载器将字节码文件加载并将将类的成员变量、构造方法、成员方法封装成相应的对象)
这样的好处:
1. 可以在程序运行过程中,操作这些对象。
2. 可以解耦,提高程序的可扩展性。
下面几个知识点可以先只瞄一眼,看最后那个代码案例的时候再对着来找知识点
获得Class对象的方式
在上面图中的三个阶段,不同阶段对应着不同的 获取Class对象的方式。在Source源码阶段(即还没将字节码加载进内存的时候用下面的方法1,在Class类对象阶段用方法2,运行时用方法3)
1. Class.forName("全类名"):将字节码文件加载进内存,返回Class对象
多用于配置文件,将类名定义在配置文件中。读取文件,加载类。
2. 类名.class:通过类名的属性class获取
多用于参数的传递,如等会代码中获取构造器就要传递 类对象。 注意:基本类型和引用类型都可以用该方法,但包装类如果要获取类对象则:通过 .TYPE 得到Class类对象
3. 对象.getClass():getClass()方法在Object类中定义着。
多用于对象的获取字节码的方式
注意:
同一个字节码文件在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的Class对象都是同一个。
Class对象功能:
获取功能: 下面中方法多了Declared的是获取所有修饰符修饰的,而没有这个的则是只获取public修饰的
1. 获取构造方法们
* Constructor<?>[] getConstructors()
* Constructor<T> getConstructor(类<?>... parameterTypes)
* Constructor<T> getDeclaredConstructor(类<?>... parameterTypes)
* Constructor<?>[] getDeclaredConstructors()
2. 获取成员变量们
* Field[] getFields()
* Field getField(String name)
* Field[] getDeclaredFields()
* Field getDeclaredField(String name)
3. 获取成员方法们:
* Method[] getMethods()
* Method getMethod(String name, 类<?>... parameterTypes)
* Method[] getDeclaredMethods()
* Method getDeclaredMethod(String name, 类<?>... parameterTypes)
4. 获取全类名(包名加类名)、获取
* String getName()
获取到的成员变量、构造方法、成员方法可以进行哪些操作
Constructor:构造方法
创建对象:
* 构造器.newInstance(参数)
* 如果使用空参数构造方法创建对象,操作可以简化:Class对象的newInstance方法
Field:成员变量
1. 设置值
* void set(Object obj, Object value)
2. 获取值
* get(Object obj)
3. 忽略访问权限修饰符的安全检查
* setAccessible(true):暴力反射
Method:方法对象
执行方法:
* invoke(Object obj, Object... args)
获取方法名称:
* String getName:获取方法名
忽略访问权限修饰符的安全检查
* setAccessible(true):暴力反射
上面知识的综合案例,一定要把所有代码和注释看懂
1.准备工作,把需要用的文件给建好 ,其中properties文件中就一个 className=com.xiaoguo.reflection.Cat,自己根据自己包在哪把Cat的全路径写上即可
2.各文件的所有代码
// Cat类下代码
public class Cat {
private String name = "招财猫";
public int age = 3;
public Cat() {
}
public Cat(String name) {
this.name = name;
}
public Cat(String name, int age) {
this.name = name;
this.age = age;
}
public void hi() {
System.out.println("hi" + name);
}
public void eat(String food) {
System.out.println(name + "正在吃" + food);
}
private void learn() {
System.out.println("这是一个私有方法");
}
@Override
public String toString() {
return "Cat{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
//Test类下全部代码
public class Test {
public static void main(String[] args) throws Exception {
//通过 Properties 来读取配置文件的信息
Properties properties = new Properties();
//此处的匿名流对象不需要关闭,
// 因为匿名对象就是在对象只使用一次的情况下创建使用,使用完后会自动回收掉,而在回收时调用的finalize方法中有关闭的操作,不需要关闭流资源
//如果不是使用的匿名流对象,那么需要关闭
properties.load(new FileReader("src/com/xiaoguo/reflection/re.properties"));
String className = properties.getProperty("className"); //注意配置文件中的className要为全类名
//通过全类名获取Class对象,并通过Class对象创建实例对象
Class cls = Class.forName(className);
//当要使用无参构造器创建对象时,可以无需先通过Class对象获取构造器,因为Class对象提供了该方法
Object o = cls.newInstance();
System.out.println("\n==========构造方法===========");
//获取无参构造器
Constructor constructor1 = cls.getConstructor();
//传入形参类型的类对象,获取含参构造器
Constructor constructor2 = cls.getConstructor(String.class);
//注意下面如果类中用的是包装类的话,要获得包装类的类对象应该是 Integer.TYPE
Constructor constructor3 = cls.getConstructor(String.class, int.class);
Object o1 = constructor3.newInstance("加菲猫", 6);
System.out.println(o1);
System.out.println("\n============字段============");
//getDeclaredFields 获取所有修饰符修饰的字段
Field[] fields = cls.getDeclaredFields();
for (Field field : fields) {
System.out.println(field);
}
//获取某个public成员变量的方法及该获取到的成员变量对象的一些方法
Field ageFiled = cls.getField("age");
System.out.println(ageFiled.get(o));
ageFiled.set(o, 18);
System.out.println(ageFiled.get(o));
//获取非public修饰的成员变量
Field nameFiled = cls.getDeclaredField("name");
//注意此时要访问的话,要去忽略访问修饰符的安全检查
nameFiled.setAccessible(true);//(暴力反射)
System.out.println(nameFiled.get(o));//没有上面这句的话会报异常 IllegalAccessException
System.out.println("\n==========方法===========");
//获取不含参的public方法
Method method1 = cls.getMethod("hi");
method1.invoke(o);
//获取含参的public方法
Method method2 = cls.getMethod("eat", String.class);
method2.invoke(o, "烤鱼");
//获取到的私有方法要使用也需要 暴力反射
Method method3 = cls.getDeclaredMethod("learn");
method3.setAccessible(true);
method3.invoke(o);
//getMethods() 获取的方法不止自己的public方法 还有其父类的public方法
//getDeclaredMethods 只会获取它自己的各修饰符修饰的方法
Method[] methods = cls.getMethods();
for (Method method : methods) {
System.out.println(method);
}
System.out.println("\n=========其它一些零碎的=========");
//得到全类名
System.out.println(cls.getName());
//得到包名
System.out.println(cls.getPackage().getName());
//看其是哪个类的Class对象
System.out.println(cls);
}
}