Java入门
IDEA优化
idea插件
翻译、阿里巴巴代码规范指导
IDEA debug使用
Step into:单步执行(一行一行代码执行),如果遇到子函数,就会进入子函数,并且继续单步执行。就是每一行需要执行的代码都不跳过,一行一行进行。
Step over:在单步执行的时候,如果遇到子函数,并不会进入子函数,而是把子函数当做一整步执行完成,从而继续执行函数调用位置下的代码。
Step out:当单步执行到子函数内时,用Step out就可以执行完子函数余下部分,并返回到上一层函数。
需要注意的是,若在接下来的代码中还存在断点时,Step over 和 Step out 不会跳过断点,也就是断点位置一定会“断”。
Java进制
- 二进制。以0b或0B开头,由数字0和1组成。
- 八进制。以0开头,由数字0到7组成。
- 十六进制。以0x或0X开头,由数字0到9以及字母A到F组成。(十六进制经常用来表示内存地址)
int i1 = 0b01100001;
System.out.printf("i1 = %d\n",i1);
int i2 = 0141;
System.out.println(i2);
int i3 = 0x61;
System.out.println(i3);
二进制计算
字符存储
其实字符并不是直接存储的,而是把每一个字符编为一个整数,存储的是字符对应整数的二进制形式。美国人搞了一套字符和整数的对应关系表,叫做ASCII编码表。
ASCII编码表中字符编码的规律:
1.字符0对应48,后面的1,2,3,4...9 对应的十进制整数依次往后顺延
2.字符a对应97,后面的b,c,d,e...z 对应的十进制整数依次往后顺延
3.字符A对应65,后面的B,C,D,E...Z 对应的十进制整数依次往后顺延
标识符
由数字、字母、下划线(_)和美元符($)等组成
不能以数字开头、不能用关键字做为名字、且是区分大小写的
命名规范:
1、不能由$、下划线、和中文,尽量不要包含数字,不能以数字开头
2、有实际含义的情况下,尽量英文的见名知意
3、变量名和方法名使用小驼峰命名法,类名使用大驼峰命名法
小驼峰:第一个单词首字母小写,其余单词首字母大写
大驼峰:所有单词首字母都大写
变量
当执行int age = 18; 这句代码时,JVM会在内存中申请一块区域,在这个区域中存储了一个整数18,给这个区域取的名字叫age; 相当于在盒子中存了一个数据18,这个盒子的名字是age,当我们打印age时,就是从盒子中把盒子中的数据取出来再打印。
变量要先声明再使用
变量的有效范围是从定义开始到“}”截止,且在同一个范围内部不能定义2个同名的变量。
变量定义的时候可以不赋初始值;但在使用时,变量里必须有值,否则报错。
字面量
/*
目标:需要同学们掌握常见数据在程序中的书写格式
*/
public class LiteralDemo{
public static void main(String[] args){
//1.整数
System.out.println(666);
//2.小数
System.out.println(3.66);
//3.字符: 字符必须用单引号引起来,不可为空
System.out.println('a');
System.out.println('0');
System.out.println('中');
System.out.println(' '); //空格也算字符
//特殊字符:\t表示制表符 \n表示换行
System.out.println('\t'); //这相当于一个tab键,专业叫做制表符
System.out.println('\n'); //这是换行的意思
//4.字符串:字符串是双引号引起来的,字符串可为空
System.out.println("我爱你中国abc");
//5.布尔值:只有两个值true和false
System.out.println(true);
System.out.println(false);
}
}
注释
1.单行注释:
//后面根解释文字
2.多行注释
/*
这里写注释文字
可以写多行
*/
3.文档注释
/**
这里写文档注释
也可以写多行,文档注释可以利用JDK的工具生成帮助文档
*/
Java执行原理
1.Java程序的执行原理是什么样的?
不管是什么样的高级编程语言,最终都是翻译成计算机底层可以识别的机器语言。
2.机器语言是由什么组成的啊?
0和1
JDK由JVM、核心类库、开发工具组成,如下图所示
下面分别介绍一下JDK中每一个部分是用来干什么的
- 什么是JVM?
答:JDK最核心的组成部分是JVM(Java Virtual Machine),它是Java虚拟机,真正运行Java程序的地方。
- 什么是核心类库?
答:它是Java本身写好的一些程序,给程序员调用的。 Java程序员并不是凭空开始写代码,是要基于核心类库提供的一些基础代码,进行编程。
- 什么是JRE?
答:JRE(Java Runtime Enviroment),意思是Java的运行环境;它是由JVM和核心类库组成的;如果你不是开发人员,只需要在电脑上安装JRE就可以运行Java程序。
- 什么是开发工具呢?
答:Java程序员写好源代码之后,需要编译成字节码,这里会提供一个编译工具叫做javac.exe,编写好源代码之后,想要把class文件加载到内存中运行,这里需要用到运行工具java.exe。
除了编译工具和运行工具,还有一些其他的反编译工具、文档工具等等...
JDK、JRE的关系用一句话总结就是:用JDK开发程序,交给JRE运行
java跨平台原理
java代码编译成字节码文件后在JVM中运行,不同的操作系统安装JVM即可,一次编译处处运行
JVM不跨平台,不同操作系统安装对应版本的JVM
helloworld
编写Java程序的步骤
编写一个Java程序需要经过3个步骤:编写代码,编译代码,运行代码
- 编写代码:任何一个文本编辑器都可以些代码,如Windows系统自带的记事本
- 编译代码:将人能看懂的源代码(.java文件)转换为Java虚拟机能够执行的字节码文件(.class文件)
- 运行代码:将字节码文件交给Java虚拟机执行
数据类型、运算符
键盘输入
// 1、导包:一般不需要我们自己做,idea工具会自动帮助我们 导包的。
// 2、抄写代码:得到一个键盘扫描器对象(东西)
Scanner sc = new Scanner(System.in);
// 3、开始 调用sc的功能,来接收用户键盘输入的数据。
System.out.println("请您输入您的年龄:");
int age = sc.nextInt(); // 执行到这儿,会开始等待用户输入一个整数,直到用户按了回车键,才会拿到数据。
System.out.println("您的年龄是:" + age);
System.out.println("请您输入您的名字:");
String name = sc.next(); // 执行到这儿,会开始等待用户输入一个字符串,直到用户按了回车键,才会拿到数据。
System.out.println(name + "欢迎您进入系统~~");
next()和nextLine()
next(),nextInt(),nextDouble(), nextShort(),nextByte()等等方法遇见第一个有效字符(非空格,非换行符、非tab)时,开始扫描,当遇见第一个空格、换行符或tab时,结束扫描,获取扫描到的内容,也就是说获得第一个扫描到的不含空格、换行符、tab的单个字符串。
nextLine():
以Enter回车键为结束符,也就是说 nextLine()方法可以接收是回车之前的所有字符。
nextLine()输入注意事项
在Java中,Scanner类用于从输入源(如键盘)读取数据。当我们使用Scanner的nextInt()、nextDouble()等方法读取数值类型的输入时,这些方法只会读取数值本身,而不会读取数值后面的换行符(即用户按下Enter键时产生的换行符)。
因此,在使用nextInt()或nextDouble()等方法读取数值输入后,换行符仍然留在输入缓冲区中。当我们接下来使用nextLine()方法尝试读取一个字符串时,nextLine()方法会立即读取缓冲区中的换行符,而不会等待用户输入新的数据。这通常会导致程序错误地认为用户已经输入了一个空字符串。
为了解决这个问题,我们在读取数值输入后,通常会添加一个额外的input.nextLine()来清除缓冲区中的换行符。这个方法会读取并丢弃换行符,从而确保下一次调用nextLine()方法时,它会等待用户输入新的数据,而不是立即读取缓冲区中的换行符。
以下是一个简单的示例:
import java.util.Scanner;
public class InputExample {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.println("请输入一个整数:");
int num = input.nextInt();
System.out.println("请输入一个字符串:");
input.nextLine(); // 清除缓冲区中的换行符
String text = input.nextLine();
System.out.println("你输入的整数是:" + num);
System.out.println("你输入的字符串是:" + text);
input.close();
}
}
在这个例子中,在读取整数后,我们使用input.nextLine()清除了缓冲区中的换行符。这样,当我们再次调用nextLine()方法以读取字符串时,它会等待用户输入新的数据,而不是立即读取换行符。
while循环输入scanner如何退出
以下三种退出方式:
while (!scanner.hasNext("0") ){
// 标志位退出
}
while (scanner.hasNextLine() ){
// 代码段
if(scanner.hasNext("0")) // 内部标志退出
break;
}
while (scanner.hasNextLine() ){
// 代码段
System.exit(0); // 系统退出
}
运算符
算数运算符
注意:在Java中两个整数相除结果还是整数。
与字符串做+运算时会被当成连接符,其结果还是字符串。
字符串的“+”操作
当“+”操作中出现字符串时,这个”+”是字符串连接符,而不是算术运算。
“itheima”+ 666 --> itheima666
在”+”操作中,如果出现了字符串,就是连接运算符,否则就是算术运算。当连续进行“+”操作时,从左到右逐个执行。
1 + 99 + "年黑马" --> 100年黑马
需要我们注意的是,自增自减只能对变量进行操作,不能操作字面量。
2.混合使用:++或者--放在变量或者前面运算规则稍有不同
//++在后:先做其他事情,再做自增和自减,等到下一次在使用这个变量时,再执行自增和自减
int a = 10;
int b = a++; //等价于 int b = a; a++;
//++在前:先自增或者自减,再做其他运算
int x = 10;
int y = --x; //等价于x--; int y = x;
int num = 10;
num++;
System.out.println(num);
int num1 = 10;
int num2 = 20;
// 10 10 20 210
int sum = (num1++) + (--num1) + (num2++) + (num2*10);
System.out.println(sum); // 250
System.out.println(num1); // 10
System.out.println(num2); // 21
// 输出:
11
250
10
21
赋值运算符
符号 | 用法 | 作用 | 底层代码形式 |
---|---|---|---|
+= | a+=b | 加后赋值 | a = (a的类型)(a + b); |
-= | a-=b | 减后赋值 | a = (a的类型)(a - b); |
*= | a*=b | 乘后赋值 | a = (a的类型)(a * b); |
/= | a/=b | 除后赋值 | a = (a的类型)(a / b); |
%= | a%=b | 取余后赋值 | a = (a的类型)(a % b); |
注意:扩展的赋值运算符隐含了强制类型转换。
面试题:
表达式的最终结果类型由表达式中的最高类型决定。
在表达式中,byte、short、char 是直接转换成int类型参与运算的。
byte x = 10;
byte y = 30;
// x = x + y; // 编译报错
// x = (byte)(x + y);
x += y; // 等价形式:x = (byte)(x + y);
System.out.println(x);
if中赋值的判断
对于if(b=true)和if(b=false)两种情况程序的执行过程是:
首先把true/false赋值给b,把true/false留在括号中。
可以理解为这时候为if(true) 和if(false)。意思就是说赋值表达式执行后会把值作为一个结果留在这里。
package com.itheima.test;
public class Test {
public static void main(String[] args) {
boolean b = true;
if (b=false){ // 赋值,false
System.out.println(1);
}else if (b) { // false
System.out.println(2);
} else if (!b){ // true
System.out.println(3);
} else {
System.out.println(4);
}
}
}
// 3
package com.itheima.test;
public class Test {
public static void main(String[] args) {
boolean b = true;
if (b=true){ // 赋值,true
System.out.println(1);
}else if (b) {
System.out.println(2);
} else if (!b){
System.out.println(3);
} else {
System.out.println(4);
}
}
}
// 1
关系运算符
符号 | 例子 | 作用 | 结果 |
---|---|---|---|
> | a>b | 判断a是否大于b | 成立返回true、不成立返回false |
>= | a>=b | 判断a是否大于或者等于b | 成立返回true、不成立返回false |
< | a<b | 判断a是否小于b | 成立返回true、不成立返回false |
<= | a<=b | 判断a是否小于或者等于b | 成立返回true、不成立返回false |
== | a==b | 判断a是否等于b | 成立返回true、不成立返回false |
!= | a != b | 判断a是否不等于b | 成立返回true、不成立返回false |
逻辑运算符
三元运算符
// 需求3:找3个整数中的较大值。
int i = 10;
int j = 45;
int k = 34;
// 找出2个整数中的较大值。
int temp = i > j ? i : j;
// 找出temp与k中的较大值。
int max2 = temp > k ? temp : k;
System.out.println(max2);
System.out.println("-----------");
System.out.println((i > j && i >k) ? i : (j > k ? j : k))
运算符优先级
从图中我们发现,&&运算比||运算的优先级高,所以&&和||同时存在时,是先算&&再算||;
比如下面的代码
//这里&&先算 相当于 true || false 结果为true
System.out.println(10 > 3 || 10 > 3 && 10 < 3); // true
最后给大家说一下,在实际开发中,其实我们很少考虑运算优先级, 因为如果你想让某些数据先运算,其实加()就可以了,这样阅读性更高。
//有括号先算 相当于 true && false 结果为false
System.out.println((10 > 3 || 10 > 3) && 10 < 3); //false
位运算
位运算指的是二进制的运算
位运算符指的是二进制位的运算,先将十进制数转成二进制后再进行运算
在二进制位运算中,1表示true,0表示false。
8>>2 = 8/4 = 2
8<<2 = 8*4 = 32
计算机默认采用大端排序,高位在左,低位在右
异或运算的特点
- 一个数,被另外一个数,异或两次,该数本身不变。
数据类型
原码、反码、补码
原码就是符号位+真值的绝对值,第一位表示符号,其余位表示值
反码:正数的反码是其本身,负数的反码是原码的符号位不变,其余位取反
补码:正数的补码是其本身,负数的补码是其反码+1
浮点数不像整数变量只用符号位和数值位就能表示,float单精度浮点数在计算机占32位(四个字节),它储存在计算机时讲32位划分为三个部分,符号位,指数和尾数。(不细讲)
基本数据类型
最高位表示正负号,0是正,1是负
举例:byte类型的范围为什么是-128~127
// 如果希望随便写一个整型字面量是long类型的,需要在其后面加上L/l
// 如果定义的数超出默认int的范围,必须在末尾加上L/l
long number4 = 73642422442424L;
// 2、浮点型
// 注意:随便写一个小数字面量,默认当成double类型对待的,如果希望这个小数是float类型的,需要在后面加上:F/f
float score1 = 99.5F;
引用数据类型
// 引用数据类型:String.
// String代表的是字符串类型,定义的变量可以用来记住字符串。
String name = "黑马";
System.out.println(name);
数据类型转换
自动类型转换
小范围-->大范围
在以上情况中,其实都会涉及到类型转换。类型转换的形式总体分为2种,一种是自动类型转换,一种是强制类型转换。 这里先学习自动类型转换
- 什么是自动类型转换呢?
答:自动类型转换指的是,数据范围小的变量可以直接赋值给数据范围大的变量
byte a = 12;
int b = a; //这里就发生了自动类型转换(把byte类型转换int类型)
- 自动类型转换的原理是怎样的?
答:自动类型转换其本质就是在较小数据类型数据前面,补了若干个字节
表达式的自动类型转换
// 面试笔试题:
byte b1 = 110;
byte b2 = 80;
int b3 = b1 + b2;
System.out.println(b3);
// 类型转换案例
byte a = 3 + 4;
因为3和4,是两个常量,Java中存在【常量优化机制】
常量优化机制:在编译时(javac),就会将3和4计算出一个7的结果,并且会自动判断该结果是否在byte取值范围内
在:编译通过
不在:编译失败
char c = 2 + '2'; // 4
强制类型转换
大范围-->小范围
目标数据类型 变量名 = (目标数据类型)被转换的数据;
注意事项
强制类型转换可能造成数据(丢失)溢出;
浮点型强转成整型,直接丢掉小数部分,保留整数部分返回
// 目标:掌握强制类型转换。
int a = 20;
byte b = (byte) a; // ALT + ENTER 强制类型转换。
System.out.println(a);
System.out.println(b);
int i = 1500;
byte j = (byte) i;
System.out.println(j);
double d = 99.5;
int m = (int) d; // 强制类型转换
System.out.println(m); // 丢掉小数部分,保留整数部分
流程控制
分支
if
switch
switch****分支的执行流程
①先执行表达式的值,再拿着这个值去与case后的值进行匹配。
②与哪个case后的值匹配为true就执行哪个case块的代码,遇到break就跳出switch分支。
③如果全部case后的值与之匹配都是false,则执行default块的代码。
if、switch的比较,各自适合什么业务场景?
if其实在功能上远远强大于switch。
if适合做条件是区间判断的情况。
switch适合做:条件是比较值的情况、代码优雅、性能较好。
switch注意事项
- 1、表达式类型只能是byte、short、int、char
JDK5开始支持枚举,JDK7开始支持String
不支持double、float、long
- 2、case给出的值不允许重复,且只能是字面量,不能是变量。
- 3、正常使用switch的时候,不要忘记写break,否则会出现穿透现象。
- 4、switch可以没有default,但是一般都会加上
- 5、case语句后面可以不加break.但是如果不加break就会出现case穿透问题:匹配哪一个case就从哪一个位置向下执行,直到遇到了break或者整体结束为止;
- 6、switch 不支持 long,是因为 switch 的设计初衷是对那些只有少数的几个值进行等值判断,如果值过于复杂,那么还是用 if 比较合适。
switch的case穿透:
增强型switch
在Java JDK 14中引入了增强的switch语句,它提供了更灵活和可读性更高的switch语法。增强的switch语句允许在switch语句中使用表达式,而不仅限于常量。它使用箭头(->)操作符将表达式与相应的代码块关联起来,包含了break。
语法
增强的switch语句的语法如下所示:
switch (expression) {
case value1 -> {
// 代码块1
}
case value2 -> {
// 代码块2
}
// 更多case语句
default -> {
// 默认代码块
}
}
特性解释
增强的switch语句具有以下特性和优势:
-
表达式匹配:增强的switch语句允许在switch语句中使用表达式,而不仅限于常量。这使得我们可以根据更复杂的条件进行匹配。
-
代码简化:增强的switch语句简化了代码,减少了冗余的代码和重复的书写。它提供了一种更紧凑的语法形式,使得代码更直观、易读。
-
可读性提高:增强的switch语句提高了代码的可读性和可维护性。它使得逻辑更清晰,更易于理解和调试。
示例
下面是一些使用增强的switch语句的示例:
使用常量匹配:
int day = 2;
String dayName = switch (day) {
case 1 -> "Monday";
case 2 -> "Tuesday";
case 3 -> "Wednesday";
case 4 -> "Thursday";
case 5 -> "Friday";
default -> "Unknown";
};
System.out.println(dayName); // 输出: Tuesday
使用表达式匹配:
int number = 15;
String numberType = switch (number % 2) {
case 0 -> "Even";
case 1 -> "Odd";
default -> "Unknown";
};
System.out.println(numberType); // 输出: Odd
使用枚举类型匹配:
enum Season {
SPRING, SUMMER, AUTUMN, WINTER
}
Season season = Season.SUMMER;
String seasonName = switch (season) {
case SPRING -> "Spring";
case SUMMER -> "Summer";
case AUTUMN -> "Autumn";
case WINTER -> "Winter";
};
System.out.println(seasonName); // 输出: Summer
以上示例展示了增强的switch语句的用法,它使得在switch语句中可以更灵活地使用表达式进行匹配,从而简化了代码并提高了可读性。
switch多值匹配
public static void main(String[] args) {
//键盘录入一个数字 赋值给变量week
Scanner sc = new Scanner(System.in);
System.out.println("请输入一个星期数");
String week = sc.next();
//用switch判断week的值 输出不同的结果
switch (week) {
case "周一", "周二", "周三" -> System.out.println("跑步");
case "周四", "周五", "周六" -> System.out.println("动感单车");
case "周日" -> System.out.println("好好吃一顿");
default -> System.out.println("请输入1~7");
}
}
switch和if的区别
布尔类型,比较区间使用if
如果判断条件是byte、short、long、int、String、枚举的常量数据,并且分支达到三个以上,适合使用switch
循环
for
如何跳出当前的多重嵌套循环
在最外层循环前加一个标记如outfor,然后用break outfor;可以跳出多重循环。例如以下代码:
public class TestBreak {
public static void main(String[] args) {
outfor: for (int i = 0; i < 10; i++){
for (int j = 0; j < 10; j++){
if (j == 5){
break outfor;
}
System.out.println("j = " + j);
}
}
}
}
面试题(重点!!!)
int a = 1;
for (int i = 0; i < 10000; i++) {
a = a++;
}
System.out.println(a);
接下来我们分析一下为什么?
count++是一个表达式,是有返回值的,它的返回值就是count自加前的值,Java对自加是这样处理的:首先把count的值(注意是值,不是引用)拷贝到一个临时变量区,然后对count变量加1,最后返回临时变量区的值。
用代码解释为这样的:
int temp = count; //先把i变量的值10保存到临时变量中
count = count+1; // i变量的值加1操作
count = temp; //再把临时变量中的值赋值给i
程序第一次循环时的详细处理步骤如下:
JVM把count值(其值是0)拷贝到临时变量区。
count值加1,这时候count的值是1。
返回临时变量区的值,注意这个值是0,没修改过。
返回值赋值给count,此时count值被重置成0。
foreach循环
结构:
for(data_type item : collections) {
...
}
int[] arr = {1, 2, 3, 4, 5, 6};
for (int i : arr) {
System.out.println(i);
} int[] arr = {1, 2, 3, 4, 5, 6};
for (int i : arr) {
System.out.println(i);
}
int sum = 0;
for (int i : arr) {
sum += i;
}
System.out.println(sum);
}
while
do while
for、while、do while区别
break和continue
- break作用:跳出并结束当前所在循环的执行
- continue作用:结束本次循环,进入下一次循环
break : 只能用于结束所在循环, 或者结束所在switch分支的执行。
continue : 只能在循环中进行使用。
随机数random
for (int i = 1; i <= 20; i++) {
// 生成:1-10之间的随机数
// 1-10 => -1 => (0 - 9) + 1
int data2 = r.nextInt(10) + 1;
System.out.println(data2);
}
System.out.println("-------------------------");
for (int i = 1;i <= 20; i++) {
// 3 - 17 => -3 => (0 - 14) + 3
int data3 = r.nextInt(15) + 3;
System.out.println(data3);
}
jdk17后随机数
可以指定区间,左闭右开
Random random = new Random();
int num = random.nextInt(6,8);
数组
Java数组
数组的静态初始化
// 目标:掌握数组的定义方式一:静态初始化数组。
// 1、数据类型[] 数组名 = new 数据类型[]{元素1, 元素2, 元素3,.....}
int[] ages = new int[]{12, 24, 36};
double[] scores = new double[]{89.9, 99.5, 59.5, 88};
// 2、简化写法:
// 数据类型[] 数组名 = {元素1, 元素2, 元素3,.....}
int[] ages2 = {12, 24, 36};
double[] scores2 = {89.9, 99.5, 59.5, 88};
// 3、数据类型[] 数组名 也可以写成 数据类型 数组名[]
int ages3[] = {12, 24, 36};
double scores3[] = {89.9, 99.5, 59.5, 88};
数组的动态初始化
数组的元素默认值
创建了数组后, 系统会给数组进行默认的初始化
整数数组,把所有元素默认初始化为0
小数数组,把所有元素默认初始化为0.0
字符数组,把所有元素默认初始化为码值为0的字符, '\u0000'
布尔数组,把所有元素默认初始化为false
引用数组,把所有元素默认初始化为null
静态和动态初始化应用场景
动态初始化:适合开始不确定具体元素值,只知道元素个数的业务场景。
静态初始化:适合一开始就知道要存入哪些元素值的业务场景。
数组在内存中如何存储
引用类型的变量里存放的都是地址值
我们以int[] ages = {12,24,36};这句话为例,看一下这句话到底在计算机中做了那些事情。
- 首先,左边int[] ages 表示定义了一个数组类型的变量,变量名叫ages
- 其次,右边{12,24,36}表示创建一个数组对象,你完全可以把它理解成一个能装数据的东西。这个对象在内存中会有一个地址值[I@4c873330,每次创建一个数组对象都会有不用的地址值。
- 然后,把右边的地址值[I@4c873330赋值给左边的ages变量
- 所以,ages变量就可以通过地址值,找到数组这个东西。
访问数组元素
数组在计算机中的执行原理
重要!!!
1、jvm内存区域划分了几块,分别存什么?
方法区、堆内存、栈内存
栈内存存放基本数据类型和引用数据类型的引用地址
堆内存存放引用数据类型
2、引用数据类型为什么叫引用数据类型?
3、字符串存在什么地方?--> 方法区的字符串常量池
总结一下int a = 10与 int[] arr = new int[]{11,22,33}的区别
-
a是一个变量,在栈内存中,a变量中存储的数据就是10这个值。
-
arr也是一个变量,在栈中,存储的是数组对象在堆内存中的地址值
多个变量指向同一个数组!!!
总结一下:
- 两个变量指向同一个数组时,两个变量记录的是同一个地址值。
- 当一个变量修改数组中的元素时,另一个变量去访问数组中的元素,元素已经被修改过了。
使用数组常见的问题
int[] a = {1, 2, 3};
int[] b = a;
System.out.println("赋值前");
System.out.println("a = " + a + ", b = " + b);
b = null;
System.out.println("赋值后");
System.out.println("a = " + a + ", b = " + b);
// 赋值前
// a = [I@2f92e0f4, b = [I@2f92e0f4
// 赋值后
// a = [I@2f92e0f4, b = null
面试题!!!
package com.itheima.test;
import java.util.Arrays;
public class Test {
public static void main(String[] args) {
int[] arr = {1,2,3};
int[] arr1 = {4,5,6};
swap(arr,arr1);
for (int i : arr) {
System.out.println(i + " ");
}
}
public static void swap(int[] arr,int[] arr1) {
int[] temp = arr;
arr = arr1;
arr1 = temp;
}
}
// 输出结果
1 2 3
package com.itheima.test;
import java.util.Arrays;
public class Test {
public static void main(String[] args) {
int[] arr = {1,2,3};
int[] arr1 = {4,5,6};
swap(arr,arr1);
for (int i : arr) {
System.out.println(i + " ");
}
}
public static void swap(int[] arr,int[] arr1) {
int[] temp = arr;
arr = arr1;
arr1 = temp;
arr1[0] = 100;
}
}
// 输出结果
100 2 3
内存图
1.内存:可以理解"内存条",任何程序,软件运行起来都会在内存中运行,占用内存,在java的世界中,将内存分为了5大块
2.分为哪5大块
栈(重点)(Stack)
主要运行方法,方法的运行都会去栈内存中运行,运行完毕之后,需要"弹栈",腾空间
堆(重点):(Heap)
每new一次,都会在堆内存中开辟空间,并为此空间自动分配一个地址值
堆中的数据都是有默认值的
整数:0
小数:0.0
字符: '\u0000'
布尔:false
引用:null
方法区(重点)(Method Area)
代码的"预备区",记录了类的信息以及方法的信息
本地方法栈(了解):专门运行native方法(本地方法)
本地方法可以理解为对java功能的扩充
有很多功能java语言实现不了,所以就需要依靠本地方法完成
寄存器(了解) -> 跟CPU有关
1.一个数组内存图
2.两个数组内存图
我们创建了两个数组,在堆内存中开辟了两个不同的空间,此时修改一个数组中的元素不会影响到另外一个数组中的数据
3.两个数组指向同一片内存空间
arr2不是new出来的,是arr1直接赋值的,arr1在内存中保存的是地址值,给了arr2,那么arr2的地址值和arr1就是一样的
所以此时arr1和arr2指向了堆内存中的同一片空间(同一个地址值,同一个数组),此时改变一个数组中的元素会影响到另外一个数组
多维数组
多维数组是数组的数组。多维数组的每个元素都是数组本身。例如,
int[][] a = new int[3][4];
在这里,我们创建了一个名为a的多维数组。它是一个二维数组,最多可以容纳12个元素,
二维数组
记住,Java使用基于零的索引,也就是说,Java中数组的索引从0开始,而不是从1开始。
初始化二维数组
int[][] a = {
{1, 2, 3},
{4, 5, 6, 9},
{7},
};
如我们所见,多维数组的每个元素都是数组本身。而且,与C / C ++不同,Java中多维数组的每一行可以具有不同的长度。
二维数组的初始化
访问二维数组
class MultidimensionalArray {
public static void main(String[] args) {
int[][] a = {
{1, -2, 3},
{-4, -5, 6, 9},
{7},
};
for (int i = 0; i < a.length; ++i) {
for(int j = 0; j < a[i].length; ++j) {
System.out.println(a[i][j]);
}
}
}
}
class MultidimensionalArray {
public static void main(String[] args) {
//创建二维数组
int[][] a = {
{1, -2, 3},
{-4, -5, 6, 9},
{7},
};
//首先for ... each循环访问单个数组
//在二维数组中
for (int[] innerArray: a) {
//第二次for ... each循环访问行内的每个元素
for(int data: innerArray) {
System.out.println(data);
}
}
}
}
二维数组内存图
public class Demo06Array {
public static void main(String[] args) {
int[][] arr1 = new int[3][];
arr1[1] = new int[]{1,2,3};
arr1[2] = new int[3];
arr1[2][1] = 100;
}
}
如何在Java中初始化三维数组?
让我们看看如何在Java中使用3d数组。我们可以初始化一个类似于2d数组的3d数组。例如,
// test is a 3d array
int[][][] test = {
{
{1, -2, 3},
{2, 3, 4}
},
{
{-4, -5, 6, 9},
{1},
{2, 3}
}
};
基本上,3d数组是2d数组的数组。三维阵列的行也可以像二维阵列一样在长度上有所变化。
示例:3维数组
class ThreeArray {
public static void main(String[] args) {
// 创建三维数组
int[][][] test = {
{
{1, -2, 3},
{2, 3, 4}
},
{
{-4, -5, 6, 9},
{1},
{2, 3}
}
};
//for..each循环迭代3d数组的元素
for (int[][] array2D: test) {
for (int[] array1D: array2D) {
for(int item: array1D) {
System.out.println(item);
}
}
}
}
}
数组复制
在Java中,我们可以将一个数组复制到另一个数组中。有几种技术可以用来在Java中复制数组。
1.使用赋值运算符复制数组(错误!)
让我们举个实例
class Main {
public static void main(String[] args) {
int [] numbers = {1, 2, 3, 4, 5, 6};
int [] positiveNumbers = numbers; //复制数组
for (int number: positiveNumbers) {
System.out.print(number + ", ");
}
}
}
输出:
1, 2, 3, 4, 5, 6
在上面的示例中,我们使用赋值运算符(=)将一个名为numbers的数组复制到另一个名为positiveEnumbers的数组中。
这种技术是最简单的一种,它同样有效。然而,这种技术有一个问题。如果我们改变一个数组的元素,其他数组的相应元素也会改变。例如:
class Main {
public static void main(String[] args) {
int [] numbers = {1, 2, 3, 4, 5, 6};
int [] positiveNumbers = numbers; //复制数组
//更改第一个数组的值
numbers[0] = -1;
//打印第二个数组
for (int number: positiveNumbers) {
System.out.print(number + ", ");
}
}
}
输出:
-1, 2, 3, 4, 5, 6
在这里,我们可以看到我们已经改变了numbers数组的一个值。当我们打印positiveEnumbers数组时,可以看到相同的值也发生了更改。
这是因为两个数组都引用相同的数组对象。这是因为浅拷贝。要了解有关浅拷贝的更多信息,请访问浅拷贝。
现在,要在复制数组的同时生成新的数组对象,我们需要深度复制而不是浅拷贝。
2.使用循环构造复制数组
让我们举个实例:
import java.util.Arrays;
class Main {
public static void main(String[] args) {
int [] source = {1, 2, 3, 4, 5, 6};
int [] destination = new int[6];
//迭代并将元素从源复制到目标
for (int i = 0; i < source.length; ++i) {
destination[i] = source[i];
}
//将数组转换为字符串
System.out.println(Arrays.toString(destination));
}
}
输出:
[1, 2, 3, 4, 5, 6]
在上面的示例中,我们使用了for循环来遍历源数组的每个元素。在每次迭代中,我们都将元素从source数组复制到destination数组。
在这里,源和目标数组引用不同的对象(深度复制)。因此,如果一个数组的元素被更改,另一个数组的相应元素也将保持不变。
注意以下语句,
System.out.println(Arrays.toString(destination));
在这里,toString()方法用于将数组转换为字符串。
3.使用arraycopy()方法复制数组
在Java中,System类包含一个名为arraycopy()的方法来复制数组。与上述两种方法相比,这种方法是一种更好的复制数组的方法。
方法允许您将源数组的指定部分复制到目标数组。例如,
arraycopy(Object src, int srcPos,Object dest, int destPos, int length)
这里,
- src -您要复制的源数组
- srcPos -源数组中的起始位置(索引)
- dest -目标数组,将从源中复制元素
- destPos -目标数组中的起始位置(索引)
- length -要复制的元素数
让我们举个实例:
//使用Arrays.toString()方法
import java.util.Arrays;
class Main {
public static void main(String[] args) {
int[] n1 = {2, 3, 12, 4, 12, -2};
int[] n3 = new int[5];
//创建长度为n1的n2数组
int[] n2 = new int[n1.length];
//将整个n1数组复制到n2
System.arraycopy(n1, 0, n2, 0, n1.length);
System.out.println("n2 = " + Arrays.toString(n2));
//从n1数组的索引2复制元素
//将元素复制到n3数组的索引1
//将复制2个元素
System.arraycopy(n1, 2, n3, 1, 2);
System.out.println("n3 = " + Arrays.toString(n3));
}
}
输出:
n2 = [2, 3, 12, 4, 12, -2]
n3 = [0, 12, 4, 0, 0]
在上面的示例中,我们使用了arraycopy()方法,
- System.arraycopy(n1, 0, n2, 0, n1.length) - 将n1数组中的整个元素复制到n2数组中
- System.arraycopy(n1, 2, n3, 1, 2 )- 从索引2开始的n1数组的两个元素被复制到从n3数组1开始的索引中
正如您看到的,int类型数组元素的默认初始值为0。
4.使用copyOfRange()方法复制数组
我们还可以使用Java Arrays类中定义的copyOfRange()方法来复制数组。例如,
//使用toString()和copyOfRange()方法
import java.util.Arrays;
class ArraysCopy {
public static void main(String[] args) {
int[] source = {2, 3, 12, 4, 12, -2};
//将整个源数组复制到目标
int[] destination1 = Arrays.copyOfRange(source, 0, source.length);
System.out.println("destination1 = " + Arrays.toString(destination1));
//从索引2复制到5(不包括5)
int[] destination2 = Arrays.copyOfRange(source, 2, 5);
System.out.println("destination2 = " + Arrays.toString(destination2));
}
}
输出结果
destination1 = [2, 3, 12, 4, 12, -2]
destination2 = [12, 4, 12]
在上面的示例中,请注意以下行:
int[] destination1 = Arrays.copyOfRange(source, 0, source.length);
在这里,我们可以看到我们正在创建destination1数组并同时将源数组复制到它。在调用copyOfRange()方法之前,我们不会创建destination1数组。要了解有关该方法的更多信息,请访问Java copyOfRange。
5.使用循环复制二维数组
类似于一维数组,我们还可以使用for循环来复制二维数组。例如,
import java.util.Arrays;
class Main {
public static void main(String[] args) {
int[][] source = {
{1, 2, 3, 4},
{5, 6},
{0, 2, 42, -4, 5}
};
int[][] destination = new int[source.length][];
for (int i = 0; i<destination.length; ++i) {
//为目标数组的每一行分配空间
destination[i] = new int[source[i].length];
for (int j = 0; j<destination[i].length; ++j) {
destination[i][j] = source[i][j];
}
}
//显示目标数组
System.out.println(Arrays.deepToString(destination));
}
}
输出:
[[1, 2, 3, 4], [5, 6], [0, 2, 42, -4, 5]]
在上面的程序中,请注意以下行:
System.out.println(Arrays.deepToString(destination);
在这里,使用deepToString()方法提供二维数组的更好表示。要了解更多信息,请访问Java deepToString()。
使用arraycopy()复制二维数组
为了使上面的代码更简单,我们可以使用System.arraycopy()替换内部循环,就像处理一维数组一样。例如,例如,
import java.util.Arrays;
class Main {
public static void main(String[] args) {
int[][] source = {
{1, 2, 3, 4},
{5, 6},
{0, 2, 42, -4, 5}
};
int[][] destination = new int[source.length][];
for (int i = 0; i<source.length; ++i) {
//为目标数组的每一行分配空间
destination[i] = new int[source[i].length];
System.arraycopy(source[i], 0, destination[i], 0, destination[i].length);
}
//显示目标数组
System.out.println(Arrays.deepToString(destination));
}
}
输出:
[[1, 2, 3, 4], [5, 6], [0, 2, 42, -4, 5]]
在这里,我们可以看到,通过用arraycopy()方法替换内部for循环,可以得到相同的输出。
方法
方法注释
方法上使用文档注释/** */
方法定义
方法的使用意义
1、不能把所有的代码都放在main中 --> 不方便维护代码
2、自由控制调用
3、实现代码的复用
方法使用的注意事项
-
方法在内种没有先后顺序,但是不能把一个方法定义在另一个方法中。
-
方法的返回值类型写void(无返回申明)时,方法内不能使用return返回数据,
如果方法的返回值类型写了具体类型,方法内部则必须使用return返回对应类型的数据。 -
return语句的下面,不能编写代码,属于无效的代码,执行不到这儿。
-
方法不调用就不会执行, 调用方法时,传给方法的数据,必须严格匹配方法的参数情况。
-
调用有返回值的方法,有3种方式:
① 可以定义变量接收结果
② 或者直接输出调用,
③ 甚至直接调用; -
调用无返回值的方法,只有1种方式: 只能直接调用。
方法在计算机中的执行原理
方法调用过程
方法没有被调用的时候,都在方法区中的字节码文件(.class)中存储
方法被调用的时候,需要进入到栈内存中运行
我们知道Java程序的运行,都是在内存中执行的,而内存区域又分为栈、堆和方法区。那Java的方法是在哪个内存区域中执行呢?
答案是栈内存。 每次调用方法,方法都会进栈执行;执行完后,又会弹栈出去。
重要!!!
1.方法的运行区域在哪里?
答:栈内存。
2.栈有什么特点?方法为什么要在栈中运行程序?
答:先进后出。保证一个方法调用完另一个方法后,可以回来继续执行。
参数传递
形参和实参
形参:全称形式参数,是指在定义方法时,所声明的参数
实参:全称实际参数,调用方法时,实际传入的参数
值类型的参数传递
Java的参数传递机制都是:值传递,传递的是实参存储的值的副本。
引用类型的参数传递
我们发现调用change方法时参数是引用类型,实际上也是值传递,只不过参数传递存储的地址值。此时change方法和main方法中两个方法中各自有一个变量arrs,这两个变量记录的是同一个地址值[I@4c873330,change方法把数组中的元素改了,main方法在访问时,元素已经被修改了。
基本类型和引用类型的参数在传递的时候有什么不同
- 都是值传递
- 基本类型的参数传递存储的数据值。
- 引用类型的参数传递存储的地址值。
java的参数传递(只有值传递)
值传递和引用传递最大的区别是传递的过程中有没有复制出一个副本来,如果是传递副本,那就是值传递,否则就是引用传递。
Java对象的传递,是通过复制的方式把引用关系传递了,因为有复制的过程,所以是值传递,只不过对于Java对象的传递,传递的内容是对象的引用。
方法重载
1.什么是方法重载?
答:一个类中,多个方法的名称相同,但它们形参列表不同。
2.方法重载需要注意什么?
- 一个类中,只要一些方法的名称相同、形参列表不同,那么它们就是方法重载了,
其它的都不管(如:修饰符,返回值类型是否一样都无所谓)。
- 形参列表不同指的是:形参的个数、类型、顺序不同,不关心形参的名称。
3、方法重载有啥应用场景?
答:开发中我们经常需要为处理一类业务,提供多种解决方案,此时用方法重载来设计是很专业的。
可变参数
在方法声明中,在指定参数类型后加一个省略号(...) 。
一个方法中只能指定一个可变参数,它必须是方法的最后一个参数。任何普通的参数必须在它之前声明。
public static void main(String[] args) {
sum(1, 2, 3);
sum(1, 2);
}
public static void sum(int... a){
int sum = 0;
for (int i = 0; i < a.length; i++) {
sum += a[i];
}
System.out.println(sum);
}
void方法中的return使用
return关键字的作用
-
只能返回一个值,并且终止方法运行
-
如果一个方法是void(没有返回值),也可以写return --> 只能终止方法运行
-
一个方法内可以有多个return,但是必须处在不同的作用域中
一个作用域只能有一个,而且一定写在该作用域的最后一行
package com.itheima.test;
public class Test {
public static void main(String[] args) {
for (int i = 0; i < 9; i++) {
if (i > 3) {
return;
}
System.out.println(i);
}
System.out.println(123);
}
}
// 输出结果
0
1
2
3
public static void main(String[] args) {
// 目标:掌握return单独使用:return; 在无返回值方法中的作用:跳出并立即结束当前方法的执行。
System.out.println("程序开始。。。");
chu(10, 0);
System.out.println("程序结束。。。");
}
public static void chu(int a, int b){
if(b == 0){
System.out.println("您的数据有问题,不能除0~~");
return; // 跳出并结束当前方法的执行。
}
int c = a / b;
System.out.println("除法结果是:" + c);
}
// 输出结果
程序开始。。。
您的数据有问题,不能除0~~
程序结束。。。
练习
验证码
public static void main(String[] args) {
// 目标:完成生成随机验证码。
System.out.println(createCode(8));
}
public static String createCode(int n){
// 1、定义一个for循环用于控制产生多少位随机字符
Random r = new Random();
// 3、定义一个String类型的变量用于记住产生的每位随机字符
String code = "";
for (int i = 1; i <= n; i++) {
// i = 1 2 3 4 5
// 2、为每个位置生成一个随机字符:可能是数字、大小写字母。
// 思路:随机一个0 1 2之间的数字出来,0代表随机一个数字字符,1、2代表随机大写字母,小写字母。
int type = r.nextInt(3); // 0 1 2
switch (type) {
case 0:
// 随机一个数字字符
code += r.nextInt(10); // 0 - 9 code = code + 8
break;
case 1:
// 随机一个大写字符 A 65 Z 65+25 (0 - 25) + 65
char ch1 = (char) (r.nextInt(26) + 65);
code += ch1;
break;
case 2:
// 随机一个小写字符 a 97 z 97+25 (0 - 25) + 97
char ch2 = (char) (r.nextInt(26) + 97);
code += ch2;
break;
}
}
return code;
}
找素数
public static void main(String[] args) {
// 目标:完成找素数。
System.out.println("当前素数的个数是:" + search(101, 200));
}
public static int search(int start, int end){
int count = 0;
// start = 101 end = 200
// 1、定义一个for循环找到101到200之间的每个数据
for (int i = start; i <= end ; i++) {
// i = 101 102 103 ... 199 200
// 信号位思想
boolean flag = true; // 假设的意思:默认认为当前i记住的数据是素数。
// 2、判断当前i记住的这个数据是否是素数。
for (int j = 2; j <= i / 2; j++) {
if(i % j == 0){
// i当前记住的这个数据不是素数了
flag = false;
break;
}
}
// 3、根据判定的结果决定是否输出i当前记住的数据:是素数才输出展示。
if(flag){
System.out.println(i);
count++;
}
}
return count;
}
数组拷贝
public static void main(String[] args) {
// 目标:掌握数组拷贝。
int[] arr = {11, 22, 33};
int[] arr2 = copy(arr);
printArray(arr2);
// 注意:这个不是拷贝数组,叫把数组变量赋值给另一个数组变量。
// int[] arr3 = arr;
// arr3[1] = 666;
// System.out.println(arr[1]);
arr2[1] = 666;
System.out.println(arr[1]);
}
public static int[] copy(int[] arr){
// arr = [11, 22, 33]
// 0 1 2
// 1、创建一个长度一样的整型数组出来。
int[] arr2 = new int[arr.length];
// arr2 = [0, 0, 0]
// 0 1 2
// 2、把原数组的元素值对应位置赋值给新数组。
for (int i = 0; i < arr.length; i++) {
// i = 0 1 2
arr2[i] = arr[i];
}
return arr2;
}
public static void printArray(int[] arr){
System.out.print("[");
for (int i = 0; i < arr.length; i++) {
System.out.print(i == arr.length - 1 ? arr[i] : arr[i] + ", ");
}
System.out.println("]");
}
方法递归
-
什么是递归?
递归是一种算法,从形式上来说,方法调用自己的形式称之为递归。
-
递归的形式:有直接递归、间接递归,如下面的代码。
/**
* 目标:认识一下递归的形式。
*/
public class RecursionTest1 {
public static void main(String[] args) {
test1();
}
// 直接方法递归
public static void test1(){
System.out.println("----test1---");
test1(); // 直接方法递归
}
// 间接方法递归
public static void test2(){
System.out.println("---test2---");
test3();
}
public static void test3(){
test2(); // 间接递归
}
}
如果直接执行上面的代码,会进入死循环,最终导致栈内存溢出
方法递归求阶乘
为了弄清楚递归的执行流程,接下来我们通过一个案例来学习一下。
案例需求:计算n的阶乘,比如5的阶乘 = 1 * 2 * 3 * 4 * 5 ; 6 的阶乘 = 1 * 2 * 3 * 4 * 5 * 6
分析需求用递归该怎么做
假设f(n)表示n的阶乘,那么我们可以推导出下面的式子
f(5) = 1*2*3*4*5
f(5) = f(4)*5
f(4) = f(3)*4
f(3) = f(2)*3
f(2) = f(1)*2
f(1) = 1
总结规律:
除了f(1) = 1; 出口
其他的f(n) = f(n-1)*n
我们可以把f(n)当做一个方法,那么方法的写法如下
package com.itheima.d2_recursion;
/**
* 目标:掌握递归的应用,执行流程和算法思想。
*/
public class RecursionTest2 {
public static void main(String[] args) {
System.out.println("5的阶乘是:" + f(5));
}
public static int f(int n){
// 终结点
if(n == 1){
return 1;
}else {
return f(n - 1) * n;
}
}
}
递归的优缺点
进行递归调用时,将在堆栈上分配新的变量存储位置。随着每个递归调用的返回,旧的变量和参数将从堆栈中删除。因此,递归通常使用更多的内存,并且通常很慢。
另一方面,递归解决方案要简单得多,并且花费更少的时间来编写,调试和维护。
方法递归在计算机的执行原理
面向对象
面向对象基础
面向对象既可以复用属性(变量),也可以复用行为
面相过程只能复用行为
对象相关注释
属性使用文档注释
创建并使用对象的流程
- 常见一个类 --> 类是对象的模板
- 给类思考对象应该具备的属性(private成员变量)和行为(没有static的成员方法)
- 生成构造器以及get/set
- 使用new 调用构造器创建对象
对象执行原理
其实Student s1 = new Student();
这句话中的原理如下
Student s1
表示的是在栈内存中,创建了一个Student类型的变量,变量名为s1- 而
new Student()
会在堆内存中创建一个对象,而对象中包含学生的属性名和属性值
同时系统会为这个Student对象分配一个地址值0x4f3f5b24 - 接着把对象的地址赋值给栈内存中的变量s1,通过s1记录的地址就可以找到这个对象
- 当执行
s1.name=“播妞”
时,其实就是通过s1找到对象的地址,再通过对象找到对象的name属性,再给对象的name属性赋值为播妞
;
搞明白Student s1 = new Student();
的原理之后,Student s2 = new Student();
原理完全一样,只是在堆内存中重新创建了一个对象,又有一个新的地址。s2.name
是访问另对象的属性。
注意事项
多个变量指向同一个对象
this
原理
this就是一个变量,用在方法中,可以拿到当前类的对象。
我们看下图所示代码,通过代码来体会这句话到底是什么意思。哪一个对象调用方法方法中的this就是哪一个对象
this应用场景
通过this在方法中可以访问本类对象的成员变量。
this主要用来解决:变量名称冲突问题的。
构造方法(构造器)
1.什么是构造器?
答:构造器其实是一种特殊的方法,但是这个方法没有返回值类型,方法名必须和类名相同。
2.构造器什么时候执行?
答:new 对象就是在执行构造方法;
3.构造方法的应用场景是什么?
答:在创建对象时,可以用构造方法给成员变量赋值
一次4.构造方法有哪些注意事项?
1)在设计一个类时,如果不写构造器,Java会自动生成一个无参数构造器。
2)一定定义了有参数构造器,Java就不再提供空参数构造器,此时建议自己加一个无参数构造器。
重点
-
构造函数在实例化对象时被调用一次,但是setter方法可以一直修改,构造器只适合使用在参数比较少的情况(7个以下)。
-
创建构造函数的两个规则是:
-
构造函数的名称应与类的名称相同。
-
Java构造函数不得具有返回类型。
-
-
如果类没有构造函数,则Java编译器会在运行时自动创建默认构造函数。默认构造函数使用默认值初始化实例变量。例如,int变量将被初始化为0
-
构造函数类型:
-
无参数构造函数 - 不接受任何参数的构造函数
-
默认构造函数 - 如果没有显式定义,Java编译器会自动创建一个构造函数。
-
参数化构造函数 - 接受参数的构造函数
-
-
构造函数不能是抽象的abstract 、static或final。
-
构造函数可以重载,但不能被重写。
封装
所谓封装,就是用类设计对象处理某一个事物的数据时,应该把要处理的数据,以及处理数据的方法,都设计到一个对象中去。
加private保证变量的安全性,选择性的提供get和set方法
封装的设计原则:合理隐藏,合理暴露
class Person {
private int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
class Main {
public static void main(String[] args) {
Person p1 = new Person();
p1.setAge(24);
System.out.println("我的年龄是 " + p1.getAge());
}
}
修饰符
数据隐藏是一种通过隐藏实现细节来限制数据成员访问的方法。
封装还提供了一种隐藏数据的方法。
数据隐藏可以通过访问修饰符来实现。在Java中,有四个访问修饰符:
Java中,可以使用访问控制符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限。
- default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
- private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
- public : 对所有类可见。使用对象:类、接口、变量、方法
- protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。
我们可以通过以下表来说明访问权限:
修饰符 | 当前类 | 同一包内 | 子孙类(同一包) | 子孙类(不同包) | 其他包 |
---|---|---|---|---|---|
public | Y | Y | Y | Y | Y |
protected | Y | Y | Y | Y/N(说明) | N |
default | Y | Y | Y | N | N |
private | Y | N | N | N | N |
protected说明
protected只能在当前类、同包或子类中访问,在其他类中创建子类的对象也无法访问
子类与基类在同一包中:被声明为 protected 的变量、方法和构造器能被同一个包中的任何其他类访问;
子类与基类不在同一包中:那么在子类中,子类实例可以访问其从基类继承而来的 protected 方法,而不能通过创建基类实例访问基类的protected方法。
package pack1;
public class Father {
protected void name() {
System.out.println("Fat.name()");
}
}
package pack2;
import pack1.Father;
public class Son extends Father {
public static void main(String[] args) {
Father f=new Father();
f.name();//The method name() from the type Father is not visible 不能通过基类实例访问protected方法
Son s=new Son();
s.name();//成功访问
}
}
总结:也就是说如果类中的某个成员被声明为protected时,那么在其他包中无法通过创建该类的实例来访问它,只能通过子类来访问它。以此实现保护的作用。
JavaBean实体类
1.JavaBean实体类是什么?有啥特点
JavaBean实体类,是一种特殊的类;它需要私有化成员变量,有无参构造方法、同时提供get和set方法;
JavaBean实体类仅仅只用来封装数据,只提供对数据进行存和取的方法
2.JavaBean的应用场景?
JavaBean实体类,只负责封装数据,而把数据处理的操作放在其他类中,以实现数据和数据处理的业务类相分离。
public class Student {
// 1、必须私有成员变量,并为每个成员变量都提供get set方法
private String name;
private double score;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getScore() {
return score;
}
public void setScore(double score) {
this.score = score;
}
// 2、必须为类提供一个公开的无参数构造器
public Student() {
}
public Student(String name, double score) {
this.name = name;
this.score = score;
}
}
成员变量和局部变量
包
- 把功能相似或相关的类或接口组织在同一个包中,方便类的查找和使用。
- 如同文件夹一样,包也采用了树形目录的存储方式。同一个包中的类名字是不同的,不同的包中的类的名字是可以相同的,当同时调用两个不同包中相同类名的类时,应该加上包名加以区别。因此,包可以避免名字冲突。
- 包也限定了访问权限,拥有包访问权限的类才能访问某个包中的类。
注意事项
- 如果当前程序中,要调用自己所在包下的其他程序,可以直接调用。(同一个包下的类,互相可以直接调用)
- 如果当前程序中,要调用其他包下的程序,则必须在当前程序中导包, 才可以访问!导包格式:import 包名.类名
- 如果当前程序中,要调用Java.lang包下的程序,不需要我们导包的,可以直接使用。
- 如果当前程序中,要调用多个不同包下的程序,而这些程序名正好一样,此时默认只能导入一个程序,另一个程序必须带包名访问。
String和ArrayList
String
// 目标:掌握创建String对象,并封装要处理的字符串的两种方式。
// 1、直接双引号得到字符串对象,封装字符串数据
String name = "黑马666";
System.out.println(name);
// 2、new String创建字符串对象,并调用构造器初始化字符串
String rs1 = new String();
System.out.println(rs1); // ""
String rs2 = new String("itheima");
System.out.println(rs2);
char[] chars = {'a', '黑', '马'};
String rs3 = new String(chars);
System.out.println(rs3);
byte[] bytes = {97, 98, 99};
String rs4 = new String(bytes);
System.out.println(rs4);
String类常用方法
- 构造方法
String(); 创建一个内容为空的字符串对象
String(char[] arr); 根据传入的字符数组创建一个字符串对象
String(char[] arr,int index,int count); 根据传入的字符数组的一部分来创建字符串对象
String(byte[] arr); 根据传入的字节数组创建一个字符串对象
String(byte[] arr,int index,int count); 根据传入的字节数组的一部分来创建字符串对象
String(String s); 根据传入的字符串来创建一个字符串对象
- 判断功能
boolean equals(String s); 判断两个字符串内容是否相同。区分大小写
boolean equalsIgnoreCase(String s); 判断两个字符串内容是否相同。不区分大小写
boolean startsWith(String s); 判断当前字符串是否以传入的字符串为开头
boolean endsWith(String s); 判断当前字符串是否以传入的字符串为结尾
boolean contains(String s); 判断当前字符串中是否包含传入的字符串
boolean isEmpty(); 判断字符串是否为空
- 获取功能
int length(); 获取字符串的长度
char charAt(int index); 获取传入的索引上的对应的字符
**int indexOf(String s);** **获取传入的字符串在当前字符串中第一次出现的索引位置**
**int lastIndexOf(String s);** **获取传入的字符串在当前字符串中最后一次出现的索引位置**
String concat(String s); 拼接字符串
String substring(int index); 从指定位置开始截取。默认到末尾
String substring(int start,int end); 截取指定范围的字符串。包含开始、不包含结束
- 转换功能
char[] toCharArray(); 将字符串转成字符数组
byte[] getBytes(); 将字符串转成字节数组
String replace(String oldStr,String newStr); 用 新字符串 替换所有的 老字符串
String replace(char oldStr,char newStr); 用 新字符 替换所有的 老字符
String toUpperCase(); 将字符串转成大写
String toLowerCase(); 将字符串转成小写
- 其他功能
String[] split(String regex); 按照指定规则切割字符串
String trim(); 去除字符串两端的空白
boolean isEmpty() 字符串长度为0返回true,反之返回false
boolean isBlank() 字符串长度为0或者仅包含空格返回true,反之返回false
// 目标:快速熟悉String提供的处理字符串的常用方法。
String s = "黑马Java";
// 1、获取字符串的长度
System.out.println(s.length());
// 2、提取字符串中某个索引位置处的字符
char c = s.charAt(1);
System.out.println(c);
// 字符串的遍历
for (int i = 0; i < s.length(); i++) {
// i = 0 1 2 3 4 5
char ch = s.charAt(i);
System.out.println(ch);
}
System.out.println("-------------------");
// 3、把字符串转换成字符数组,再进行遍历
char[] chars = s.toCharArray();
for (int i = 0; i < chars.length; i++) {
System.out.println(chars[i]);
}
// 4、判断字符串内容,内容一样就返回true
String s1 = new String("黑马");
String s2 = new String("黑马");
System.out.println(s1 == s2); // false
System.out.println(s1.equals(s2)); // true
// 5、忽略大小写比较字符串内容
String c1 = "34AeFG";
String c2 = "34aEfg";
System.out.println(c1.equals(c2)); // false
System.out.println(c1.equalsIgnoreCase(c2)); // true
// 6、截取字符串内容 (包前不包后的)
String s3 = "Java是最好的编程语言之一";
String rs = s3.substring(0, 8);
System.out.println(rs);
// 7、从当前索引位置一直截取到字符串的末尾
String rs2 = s3.substring(5);
System.out.println(rs2);
// 8、把字符串中的某个内容替换成新内容,并返回新的字符串对象给我们
String info = "这个电影简直是个垃圾,垃圾电影!!";
String rs3 = info.replace("垃圾", "**");
System.out.println(rs3);
// 9、判断字符串中是否包含某个关键字
String info2 = "Java是最好的编程语言之一,我爱Java,Java不爱我!";
System.out.println(info2.contains("Java"));
System.out.println(info2.contains("java"));
System.out.println(info2.contains("Java2"));
// 10、判断字符串是否以某个字符串开头。
String rs4 = "张三丰";
System.out.println(rs4.startsWith("张"));
System.out.println(rs4.startsWith("张三"));
System.out.println(rs4.startsWith("张三2"));
// 11、把字符串按照某个指定内容分割成多个字符串,放到一个字符串数组中返回给我们
String rs5 = "张无忌,周芷若,殷素素,赵敏";
String[] names = rs5.split(",");
for (int i = 0; i < names.length; i++) {
System.out.println(names[i]);
}
// indexOf() 返回值
// 返回指定字符/字符串的第一个匹配项的索引
// 如果找不到指定的字符/字符串,则返回 -1。
String str1 = "Learn Java";
int result;
//获取字符“ J”的索引
result = str1.indexOf('J');
System.out.println(result); // 6
//获取“ ava”的索引
result = str1.indexOf("ava");
System.out.println(result); // 7
//子字符串不在字符串中
result = str1.indexOf("java");
System.out.println(result); // -1
//字符串中空字符串的索引
result = str1.indexOf("");
System.out.println(result); // 0
}
注意事项
注意事项1:String类的对象是不可变的对象
String是一种不可变对象. 字符串中的内容是不可改变。字符串不可被修改
“”和new字符串区别
- 在JDK6.0及之前版本,字符串常量池是放在Perm Gen区(也就是方法区)中;
- 在JDK7.0版本,字符串常量池被移到了堆中了。
- 只要是以“...”方式写出的字符串对象,会存储到字符串常量池,且相同内容的字符串只存储一份
- 但通过new方式创建字符串对象,每new一次都会产生一个新的对象放在堆内存中。
面试
String[] arr = {"你","你","你"};
System.out.println(arr[0]==arr[1]);
System.out.println(arr[0]==arr[2]);
// true true
""和new字符串的内存图
面试
StringBuilder sb = new StringBuilder("你好");
StringBuilder sb1 = new StringBuilder("你好");
System.out.println(sb==sb1); // false
String s = sb.toString();
String s1 = sb1.toString();
System.out.println(s==s1); // false
String s = new String("hello");
char[] c = {'h','e','l','l','o'};
System.out.println(s.equals(c)); // false
字符串常量池复用机制
如果一个常量已经存在常量池汇总,那么不会重复创建,而是使用之前已经存在的常量
随机产生验证码
public static void main(String[] args) {
System.out.println(createCode(4));
System.out.println(createCode(6));
}
/**
1、设计一个方法,返回指定位数的验证码
*/
public static String createCode(int n){
// 2、定义2个变量 一个是记住最终产生的随机验证码 一个是记住可能用到的全部字符
String code = "";
String data = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
Random r = new Random();
// 3、开始定义一个循环产生每位随机字符
for (int i = 0; i < n; i++) {
// 4、随机一个字符范围内的索引。
int index = r.nextInt(data.length());
// 5、根据索引去全部字符中提取该字符
code += data.charAt(index); // code = code + 字符
}
// 6、返回code即可
return code;
}
为什么StringBuilder类的append方法可以提高字符串拼接的效率
如果用+拼接,每次都会创建一个新的StringBuilder对象,调用append方法,并且最后调用toString方法
如果使用StringBuilder来拼接的话,只会创建一个StringBuilder对象,调用一次toString方法
ArrayList
ArrayList类是List接口的实现,允许我们创建可调整大小的数组。
Java 数组 与 ArrayList
在Java中,我们需要先声明数组的大小,然后才能使用它。一旦声明了数组的大小,就很难更改它。
要解决此问题,我们可以使用ArrayList类。 java.util包中存在的ArrayList类允许我们创建可调整大小的数组。
与数组不同,当我们向数组列表添加或删除元素时,数组列表(ArrayList类的对象)可以自动调整其容量。 因此,数组列表也称为动态数组。
创建ArrayList
这是我们可以在Java中创建数组列表的方法:
ArrayList
此处,Type指示集合元素的类型。例如,
//创建整数类型arraylist
ArrayList<Integer> arrayList = new ArrayList<>();
//创建字符串类型arraylist
ArrayList<String> arrayList = new ArrayList<>();
在上面的程序中,我们使用了Integer和String。 在这里,Integer是int类型的相应包装类。
包装类是包装原始数据类型的类。例如,Integer类包装了int类型,Float类包装了Float类型,等等。
注意:我们不能创建原始数据类型(如int,float,char等)的集合。相反,我们必须使用它们对应的包装器类。
对于字符串,String是一个类,没有包装类。因此,我们按原样使用String。
我们还可以使用List接口创建ArrayList。这是因为ArrayList类实现了List接口。
List
方法
public static void main(String[] args) {
// 1、创建一个ArrayList的集合对象
// ArrayList<String> list = new ArrayList<String>();
// 从jdk 1.7开始才支持的
ArrayList<String> list = new ArrayList<>();
list.add("黑马");
list.add("黑马");
list.add("Java");
System.out.println(list);
// 2、往集合中的某个索引位置处添加一个数据
list.add(1, "MySQL");
System.out.println(list);
// 3、根据索引获取集合中某个索引位置处的值
String rs = list.get(1);
System.out.println(rs);
// 4、获取集合的大小(返回集合中存储的元素个数)
System.out.println(list.size());
// 5、根据索引删除集合中的某个元素值,会返回被删除的元素值给我们
System.out.println(list.remove(1));
System.out.println(list);
// 6、直接删除某个元素值,删除成功会返回true,反之
System.out.println(list.remove("Java"));
System.out.println(list);
list.add(1, "html");
System.out.println(list);
// 默认删除的是第一次出现的这个黑马的数据的
System.out.println(list.remove("黑马"));
System.out.println(list);
// 7、修改某个索引位置处的数据,修改后会返回原来的值给我们
System.out.println(list.set(1, "黑马程序员"));
System.out.println(list);
}
案例-从集合中删除指定元素
接下来,我们学习一个ArrayList的应用案例,需求如下:
我们分析一下这个案例的步骤该如何实现:
1.用户可以选购多个商品,可以创建一个ArrayList集合,存储这些商品
2.按照需求,如果用户选择了"枸杞"批量删除,应该删除包含"枸杞"的所有元素
1)这时应该遍历集合中每一个String类型的元素
2)使用String类的方法contains判断字符串中是否包含"枸杞"
3)包含就把元素删除
3.输出集合中的元素,看是否包含"枸杞"的元素全部删除
按照分析的步骤,完成代码
public class ArrayListTest2 {
public static void main(String[] args) {
// 1、创建一个ArrayList集合对象
ArrayList<String> list = new ArrayList<>();
list.add("枸杞");
list.add("Java入门");
list.add("宁夏枸杞");
list.add("黑枸杞");
list.add("人字拖");
list.add("特级枸杞");
list.add("枸杞子");
System.out.println(list);
//运行结果如下: [Java入门, 宁夏枸杞, 黑枸杞, 人字拖, 特级枸杞, 枸杞子]
// 2、开始完成需求:从集合中找出包含枸杞的数据并删除它
for (int i = 0; i < list.size(); i++) {
// i = 0 1 2 3 4 5
// 取出当前遍历到的数据
String ele = list.get(i);
// 判断这个数据中包含枸杞
if(ele.contains("枸杞")){
// 直接从集合中删除该数据
list.remove(ele);
}
}
System.out.println(list);
//删除后结果如下:[Java入门, 黑枸杞, 人字拖, 枸杞子]
}
}
运行完上面代码,我们会发现,删除后的集合中,竟然还有黑枸杞,枸杞子在集合中。这是为什么呢?
枸杞子被保留下来,原理是一样的。可以自行分析。
那如何解决这个问题呢?这里打算给大家提供两种解决方案:
- 集合删除元素方式一:每次删除完元素后,让控制循环的变量i--就可以了;如下图所示
具体代码如下:
// 方式一:每次删除一个数据后,就让i往左边退一步
for (int i = 0; i < list.size(); i++) {
// i = 0 1 2 3 4 5
// 取出当前遍历到的数据
String ele = list.get(i);
// 判断这个数据中包含枸杞
if(ele.contains("枸杞")){
// 直接从集合中删除该数据
list.remove(ele);
i--;
}
}
System.out.println(list);
- 集合删除元素方式二:我们只需要倒着遍历集合,在遍历过程中删除元素就可以了
具体代码如下:
// 方式二:从集合的后面倒着遍历并删除
// [Java入门, 人字拖]
// i
for (int i = list.size() - 1; i >= 0; i--) {
// 取出当前遍历到的数据
String ele = list.get(i);
// 判断这个数据中包含枸杞
if(ele.contains("枸杞")){
// 直接从集合中删除该数据
list.remove(ele);
}
}
System.out.println(list);
==和equals的区别
"=="和equals区别
- "=="是运算符,如果是基本数据类型,则比较存储的值;如果是引用数据类型,则比较所指向对象的地址值。
- equals是Object的方法,比较的是所指向的对象的地址值,一般情况下,重写之后比较的是对象的值。基本数据类型不能使用equals方法进行比较
String类的==和equals方法
一般情况下,类会重写equals方法用来比较两个对象的内容是否相等。比如String类中的equals()是被重写了,比较的是对象的值。
如果类没有重写equals方法,其比较的还是引用类型的地址。
public static void main(String[] args) {
//基本数据类型的比较
int num1 = 10;
int num2 = 10;
System.out.println(num1 == num2); //true
//引用数据类型的比较
//String类(重写了equals方法)中==与equals的比较
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); //true,比较地址值:内容相同,因为常量池中只有一个“hello”,所以它们的地址值相同
System.out.println(s1.equals(s2));//true,比较内容:内容相同,因为常量池中只有一个“hello”,所以它们的地址值相同
System.out.println(s1.equals("hello")); //true
String s3 = new String("hello");
String s4 = new String("hello");
System.out.println(s3 == s4); //false,比较地址值:s3和s4在堆内存中的地址值不同
System.out.println(s3.equals(s4)); //true,比较内容:内容相同
//没有重写equals方法的类中==与equals的比较
People p1 = new People();
People p2 = new People();
People p = p2;
System.out.println(p1);//People@135fbaa4
System.out.println(p2);//People@45ee12a7
System.out.println(p); //People@45ee12a7
System.out.println(p1.equals(p2)); //false,p1和p2的地址值不同
System.out.println(p.equals(p2)); //true,p和p2的地址值相同
}
创建字符串对象一般有如下两种写法
String s1 = "hello";//在字符串常量池中创建"hello",并将地址值赋值给s1。
String s2 = new String("world");//通过new关键字在堆中创建对象,并将对象地址值赋值给s2。
• 对于String s2 = new String(“world”);
首先在堆内存中申请内存存储String类型的对象,将地址值赋给s2;
在方法区的常量池中找,有无hello:
若没有,则在常量池中开辟空间存储hello,并将该空间的地址值赋给堆中存储对象的空间;
若有,则直接将hello所在空间的地址值给堆中存储对象的空间。
• 对于String s1 = “hello”;
在方法区的常量池中找,有无hello,如果没有,就在常量池中开辟空间存储hello。
然后只需要将hello所在空间的地址值赋给 s1。
字符串作为最基础的数据类型,使用非常频繁,如果每次都通过 new 关键字进行创建,会耗费高昂的时间和空间代价。Java 虚拟机为了提高性能和减少内存开销,就设计了字符串常量池. 在JDK1.7之前字符串常量池是存储在方法区的。JDK1.7之后存储在堆中了。
为什么重写equals方法就一定要重写hashCode方法
hashCode 和 equals 两个方法是用来协同判断两个对象是否相等的,采用这种方式的原因是可以提高程序插入和查询的速度。
如果只重写equals方法,不重写hashCode方法,就有可能导致a.equals(b)这个表达式成立,但是hashCode却不同。会造成一个完全相同的对象会存储在hash表的不同位置。
为什么要重写 equals 方法
Object 类中的 equals 方法用于检测一个对象是否等于另外一个对象。在 Object 类中,这个方法将判断两个对象是否具有相同的引用。如果两个对象具有相同的引用,它们一定是相等的。大多数情况下不重写equals方法,直接使用Object中的equals方法是没有任何意义的,不如直接使用==运算符,二者是等价的。
两个不同的引用,不重写equals,直接调用Object类中的原始equals方法,比较结果一定是false,这种比较没有意义,因为内存地址不同,即使两个对象的内容完全相同,因此通常情况下,我们要判断两个对象是否相等,一定要重写 equals 方法,这就是为什么要重写 equals 方法的原因。
实例练习
1、下面的代码将创建几个字符串对象?3个
String s1 = new String(“Hello”);
String s2 = new String(“Hello”);
分别创建了s1堆内存上的实例,s2内存上的实例,以及字符串常量池
2.在java中,String s=new String(“xyz”)创建了几个对象?C
A 1个 B 1个或2个 C 2个 D 以上都不对
3.下面的代码输出什么?
String s1 = new String(“abc”);
String s2 = new String(“abc”);
System.out.println(s1 == s2);//false
System.out.println(s1.equals(s2)); // true
4.下面的代码输入什么?
String s1 = "abc"; // s1指向常量池的abc
String s2 = new String("abc"); // s2指向堆内存的abc
//可以拿到s2的常量
s2.intern(); //返回常量池中abc的引用
System.out.println(s1 ==s2); //false
5.下面的代码输出什么?
String s1= "abc";
String s2= "abc";
String s3 = new String("abc");
String s4 = new String("abc");
System.out.println("s3 == s4 : "+(s3==s4)); //false
System.out.println("s3.equals(s4) : "+(s3.equals(s4))); //true
System.out.println("s1 == s3 : "+(s1==s3)); //false
System.out.println("s1.equals(s3) : "+(s1.equals(s3))); //true
System.out.println(s1==s2); //true
6.下面的代码输出什么?
String str1 = “ab” + “cd”;
String str11 = “abcd”;
System.out.println("str1 = str11 : "+ (str1 == str11)); //true
7.下面的代码输出什么?
String str2 = “ab”;
String str3 = “cd”;
String str4 = str2+str3;
String str5 = “abcd”;
System.out.println("str4 = str5 : " + (str4==str5));//false
8.下面的代码输出什么?(出现final的地方都会被换成其记住的字面量)
final String str2 = “ab”;
final String str3 = “cd”;
String str4 = str2+str3;
String str5 = “abcd”;
System.out.println("str4 = str5 : " + (str4==str5));//true
9.下面的代码输入什么?(常量字符串和变量拼接时(如:String str3=baseStr + “01”;)会调用StringBuilder.append()在堆上创建新的对象。)
String str6 = “b”;
String str7 = “a” + str6;
String str67 = “ab”;
System.out.println("str7 = str67 : "+ (str7 == str67)); //false
10.下面的代码输入什么?
final String str8 = “b”;
String str9 = “a” + str8;
String str89 = “ab”;
System.out.println("str9 = str89 : "+ (str9 == str89)); //true
11.下面选项结果为true的是:C
String s1="Hello";
String s2="hello";
String s3=s1.toLowerCase();
String s4=s2.toLowerCase();
A.s1==s3
B.s2==s3
C.s2==s4
D.s3==s4
因为String类是不可变类,调用toLowerCase()函数不会修改原始字符串,而是返回一个新的字符串。因此,如果需要使用转换后的字符串,一定要将它赋值给一个新的变量。
当传入的字符串与转换为小写字母后的字符串相同时,返回的就是原字符串,所以输出判断时,字符串常量池已有指定的内容”hello“,直接进行引用即可,并没有开辟新的空间,所以地址相同
传入的字符串与转换为小写字母后的字符串不同时,源码中指出此时返回的是new出来的String对象,而只要是构造方法new出来的对象都是要在堆中另开辟空间的,所以两者的地址不同,返回false;
String类中intern的用法:
// jdk 1.7之后intern的用法:
// intern方法还是会先去查询常量池中是否有已经存在,
// 如果存在,则返回常量池中的引用,这一点与之前jdk 1.6没有区别,区别在于,
// 如果在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而是在常量池中生成一个对原字符串的引用。
// 简单的说,就是往常量池放的东西变了:原来jdk 1.6在常量池中找不到时,复制一个副本放到常量池,
// 1.7后则是将在堆上的地址引用复制到常量池。
// String s1 = new String("a"); // 堆内存和常量池中,s1指向堆内存的a
// s1.intern(); // 常量池中已经存在,返回常量池a的引用
// String s2 = "a"; // 常量池中存在,s2指向常量池的a
// System.out.println(s1==s2); // false s1指向堆内存,s2指向常量池
// System.out.println(s1.intern() == s2); //true 都指向常量池里的a
// String s1 = new String("a"); // 堆内存和常量池中,s1指向堆内存的a
// String s2 = "a"; // 常量池中存在,s2指向常量池的a
// s1.intern(); // 常量池中已经存在,返回常量池a的引用
// System.out.println(s1==s2); // false s1指向堆内存,s2指向常量池
// System.out.println(s2 == s1.intern()); // true 都指向常量池的a
// String s1 = new String("a")+new String("b"); // s1指向堆内存中的ab
// s1.intern(); // 常量池中没有ab,jdk 1.7后常量池存放的是堆内存中ab的引用
// String s2 = "ab"; // 返回常量池中的那个引用,s2实际指向堆内存中的ab
// System.out.println(s1==s2); // true
// System.out.println(s1.intern() == s2); // true 实际都指向堆内存的ab
// String s1 = new String("a")+new String("b"); // s1指向堆内存中的ab
// String s2 = "ab"; // 常量池中不存在,创建ab,s2指向常量池中的ab
// s1.intern(); // 常量池中已经存在,返回常量池ab的引用
// System.out.println(s1==s2); // false
// System.out.println(s1.intern() == s2); // true 都指向常量池的ab
String类中replace的用法
- replace方法是区分大小写的,意味着它会将字符串中完全匹配的字符或子字符串替换为新的字符或子字符串。
- 如果原始字符串中不存在要替换的字符或子字符串,则replace方法不会进行任何替换,直接返回原始字符串。
- replace方法返回的是一个新的字符串,不会修改原始字符串对象。
String类中大小写转换的使用注意事项
在Java中,可以使用String
类的toLowerCase()
方法将字符串转换为小写,使用toUpperCase()
方法将字符串转换为大写。这两个方法都不会改变原始字符串,而是返回一个新的字符串。
注意事项:
- 如果原始字符串中包含特殊字符或者非字母字符,这些字符不会被转换,会直接保留在结果字符串中。
- 如果原始字符串中的字符已经是小写或大写,它们会保持不变。
- 这两个方法都不会对字符串进行原地转换,而是返回一个新的字符串。
不同JDK版本的新特性
垃圾回收机制
概述
java相较于c、c++语言的优势之一是自带垃圾回收器,垃圾回收是指不定时去堆内存中清理不可达对象。不可达的对象并不会马上就会直接回收, 垃圾收集器在一个Java程序中的执行是自动的,不能强制执行,程序员唯一能做的就是通过调用System.gc 方法来建议执行垃圾收集器,但其是否可以执行,什么时候执行却都是不可知的。这也是垃圾收集器的最主要的缺点。当然相对于它给程序员带来的巨大方便性而言,这个缺点是瑕不掩瑜的。
不同的虚拟机实现有着不同的 GC 实现机制,但是一般情况下每一种 GC 实现都会在以下两种情况下触发垃圾回收。
Allocation Failure
:在堆内存中分配时,如果因为可用剩余空间不足导致对象内存分配失败,这时系统会触发一次 GC。System.gc()
:在应用层,Java 开发工程师可以主动调用此 API 来请求一次 GC。
1、寻找垃圾
一般有两种方法来判断:
引用计数器法:为每个对象创建一个引用计数,有对象引用时计数器 +1,引用被释放时计数 -1,当计数器为 0 时就可以被回收。但是他有一个缺点是不能解决循环引用的问题。
可达性分析算法:从 GC Root 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Root 没有任何引用链相连时,则证明此对象是可以被回收的。
引用计数器算法
1.引用计数算法(Reference Counting)比较简单,对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。
2、对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
3、优点: 实现简单,垃圾对象便于辨识; 判定效率高,回收没有延迟性。
4、缺点:
1、它需要单独的字段存储计数器,这样的做法增加了存储 空间的开销。
2、每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了 时间开销。
3、引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。
可达性分析
JVM
把内存中所有的对象之间的引用关系看作一张图,通过一组名为”GC Root
"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,最后通过判断对象的引用链是否可达来决定对象是否可以被回收。如下图所示:
注意:上图中圆形图标虽然标记的是对象,但实际上代表的是此对象在内存中的引用。包括 GC Root 也是一组引用而并非对象。
GC Root 对象
在 Java 中,有以下几种对象可以作为 GC Root
:
- Java 虚拟机栈(局部变量表)中的引用的对象。
- 方法区中静态引用指向的对象。
- 仍处于存活状态中的线程对象。
- Native 方法中 JNI 引用的对象。
注意:全局变量同静态变量不同,它不会被当作 GC Root。
2、回收垃圾
标记清除算法(Mark and Sweep GC)
从”GC Roots
”集合开始,将内存整个遍历一次,保留所有可以被 GC Roots
直接或间接引用到的对象,而剩下的对象都当作垃圾对待并回收,过程分两步。
- Mark 标记阶段:找到内存中的所有 GC Root 对象,只要是和 GC Root 对象直接或者间接相连则标记为灰色(也就是存活对象),否则标记为黑色(也就是垃圾对象)。
- Sweep 清除阶段:当遍历完所有的 GC Root 之后,则将标记为垃圾的对象直接清除。
标记清除算法
- 优点:实现简单,不需要将对象进行移动。
- 缺点:这个算法需要中断进程内其他组件的执行(stop the world),并且可能产生内存碎片,提高了垃圾回收的频率。
标记-压缩算法 (Mark-Compact)
需要先从根节点开始对所有可达对象做一次标记,之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。最后,清理边界外所有的空间。因此标记压缩也分两步完成:
- Mark 标记阶段:找到内存中的所有 GC Root 对象,只要是和 GC Root 对象直接或者间接相连则标记为灰色(也就是存活对象),否则标记为黑色(也就是垃圾对象)。
- Compact 压缩阶段:将剩余存活对象按顺序压缩到内存的某一端。
- 优点:这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。
- 缺点:所谓压缩操作,仍需要进行局部对象移动,所以一定程度上还是降低了效率。
复制算法(Copying)
将现有的内存空间分为两快,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中。之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
- 复制算法之前,内存分为 A/B 两块,并且当前只使用内存 A,内存的状况如下图所示:
2.标记完之后,所有可达对象都被按次序复制到内存 B 中,并设置 B 为当前使用中的内存。内存状况如下图所示:
- 优点:按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。
- 缺点:可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制。
总结
3、JVM分代回收
Java 虚拟机根据对象存活的周期不同,把堆内存划分为几块,一般分为新生代、老年代,这就是 JVM 的内存分代策略。注意: 在 HotSpot 中除了新生代和老年代,还有永久代。
新生代
新生成的对象优先存放在新生代中,新生代对象存活率很低,在新生代中,常规应用进行一次垃圾收集一般可以回收 70%~95% 的空间,回收效率很高。新生代中因为要进行一些复制操作,所以一般采用的 GC 回收算法是复制算法。 新生代又可以继续细分为 3 部分:Eden
、Survivor0(简称 S0)
、Survivor1(简称S1)
。这 3 部分按照 8:1:1 的比例来划分新生代。
- 绝大多数刚刚被创建的对象会存放在 Eden 区。
- 当
Eden
区第一次满的时候,会进行垃圾回收。首先将Eden
区的垃圾对象回收清除,并将存活的对象复制到S0
,此时S1
是空的。 - 下一次
Eden
区满时,再执行一次垃圾回收。此次会将Eden
和S0
区中所有垃圾对象清除,并将存活对象复制到S1
,此时S0
变为空。 - 如此反复在
S0
和S1
之间切换几次(默认 15 次)之后,如果还有存活对象。说明这些对象的生命周期较长,则将它们转移到老年代中。
老年代
一个对象如果在新生代存活了足够长的时间而没有被清理掉,则会被复制到老年代。老年代的内存大小一般比新生代大,能存放更多的对象。如果对象比较大(比如长字符串或者大数组),并且新生代的剩余空间不足,则这个大对象会直接被分配到老年代上。
我们可以使用 -XX:PretenureSizeThreshold
来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上。老年代因为对象的生命周期较长,不需要过多的复制操作,所以一般采用标记压缩的回收算法。
注意:对于老年代可能存在这么一种情况,老年代中的对象有时候会引用到新生代对象。这时如果要执行新生代 GC,则可能需要查询整个老年代上可能存在引用新生代的情况,这显然是低效的。所以,老年代中维护了一个 512 byte 的
card table
,所有老年代对象引用新生代对象的信息都记录在这里。每当新生代发生 GC 时,只需要检查这个card table
即可,大大提高了性能。
static修饰符
static 修饰符,用来修饰类方法和类变量。
类变量
- 静态变量:static 关键字用来声明独立于对象的静态变量,无论一个类示例化多少对象,它的静态变量只有一份拷贝。 静态变量也被称为类变量。局部变量不能被声明为 static 变量。
由于静态变量是属于类的,所有对象共有一份,只需要通过类名就可以调用:类名.静态变量
实例变量是属于对象的,每个对象都有一份,需要通过对象才能调用:对象.实例变量
类变量的应用场景
在实际开发中,如果某个数据只需要一份,且希望能够被共享(访问、修改),则该数据可以定义成类变量来记住。
类方法
- 类方法:static修饰的方法,可以被类名调用,是因为它是随着类的加载而加载的;所以类名直接就可以找到static修饰的方法
- 实例方法:非static修饰的方法,需要创建对象后才能调用,是因为实例方法中可能会访问实例变量,而实例变量需要创建对象后才存在。所以实例方法,必须创建对象后才能调用。
类方法的应用场景
学习完static修饰方法之后,我们讲一个有关类方法的应用知识,叫做工具类。
如果一个类中的方法全都是静态的,那么这个类中的方法就全都可以被类名直接调用,由于调用起来非常方便,就像一个工具一下,所以把这样的类就叫做工具类。
工具类没有创建对象的需求,建议将工具类的构造器私有化
类方法的注意事项
类方法中可以直接访问类的成员,不可以直接访问实例成员。
实例方法中既可以直接访问类成员,也可以直接访问实例成员。
实例方法中可以出现this关键字,类方法中不可以出现this关键字的。
package com.itheima.d4_static_attention;
public class Student {
static String schoolName; // 类变量
double score; // 实例变量
// 1、类方法中可以直接访问类的成员,不可以直接访问实例成员。
public static void printHelloWorld(){
// 注意:同一个类中,访问类成员,可以省略类名不写。
schoolName = "黑马";
printHelloWorld2();
// System.out.println(score); // 报错的
// printPass(); // 报错的
// System.out.println(this); // 报错的
}
// 类方法
public static void printHelloWorld2(){
}
// 2、实例方法中既可以直接访问类成员,也可以直接访问实例成员。
// 实例方法
// 3、实例方法中可以出现this关键字,类方法中不可以出现this关键字的
public void printPass(){
schoolName = "黑马2";
printHelloWorld2();
System.out.println(score);
printPass2();
System.out.println(this);
}
// 实例方法
public void printPass2(){
}
}
static代码块和实例代码块
父子类的静态变量、静态代码块、实例变量、实例代码块和构造函数的执行顺序
静态变量和静态语句块优先于实例变量和普通语句块,静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。
public static String staticField = "静态变量";
static {
System.out.println("静态代码块");
}
public String field = "实例变量";
{
System.out.println("实例代码块");
}
最后才是构造函数的初始化。
public InitialOrderTest() {
System.out.println("构造函数");
}
存在继承的情况下,初始化顺序为:
- 父类(静态变量、静态代码块)
- 子类(静态变量、静态语代码块)
- 父类(实例变量、实例代码块)
- 父类(构造方法)
- 子类(实例变量、实例代码块)
- 子类(构造方法)
单例设计模式
设计模式
设计模式是解决问题的最优解
饿汉式单例设计模式
拿对象时对象就已经创建好了
package com.itheima.sta.singleton;
public class A {
// 定义一个类变量用于接受类的一个对象
private static A a = new A();
// 外界就无法创建对象
private A(){}
// 提供一个公共的方法来获取当前类的对象
public static A getInstance() {
return a;
}
}
package com.itheima.sta.singleton;
public class DemoTest {
public static void main(String[] args) {
A a = A.getInstance();
A a1 = A.getInstance();
System.out.println(a);
System.out.println(a1);
System.out.println(a == a1); // true
}
}
懒汉式单例设计模式
单例类的应用场景
继承
继承在计算机中的原理
使用继承的优势
减少代码的重复编写,提高代码的复用性
单继承和多层继承
Java语言只支持单继承,不支持多继承,但是可以多层继承。
Java是单继承的:一个类只能继承一个直接父类;
Object类是Java中所有类的父类。
public class Test {
public static void main(String[] args) {
// 目标:掌握继承的两个注意事项事项。
// 1、Java是单继承的:一个类只能继承一个直接父类;
// 2、Object类是Java中所有类的祖宗。
A a = new A();
B b = new B();
ArrayList list = new ArrayList();
list.add("java");
System.out.println(list.toString());
}
}
class A {} //extends Object{}
class B extends A{}
// class C extends B , A{} // 报错
class D extends B{}
方法重写
当子类觉得父类方法不好用,或者无法满足父类需求时,子类可以重写一个方法名称、参数列表一样的方法,去覆盖父类的这个方法,这就是方法重写。
注意:重写后,方法的访问遵循就近原则。
注意事项
- 重写小技巧:使用Override注解,他可以指定java编译器,检查我们方法重写的格式是否正确,代码可读性也会更好。
- 子类重写父类方法时,访问权限必须大于或者等于父类该方法的权限( public> protected > 缺省)。
- 重写的方法返回值类型,必须与被重写方法的返回值类型一样,或者范围更小。
- 私有方法、静态方法不能被重写,如果重写会报错的。(静态方法不能被重写是因为静态方法是属于类的,不属于实例化对象,所以没有多态的说法,自然就不能被重写)
子类中访问成员的原则
在子类方法中访问其他成员(成员变量、成员方法),是依照就近原则的
-
先子类局部范围找。
-
然后子类成员范围找。
-
然后父类成员范围找,如果父类范围还没有找到则报错。
如果子类和父类出现同名变量或者方法,优先使用子类的;此时如果一定要在子类中使用父类的成员,可以加this或者super进行区分。
public class Z extends F {
String name = "子类名称";
public void showName(){
String name = "局部名称";
System.out.println(name); // 局部名称
System.out.println(this.name); // 子类成员变量
System.out.println(super.name); // 父类的成员变量
}
@Override
public void print1(){
System.out.println("==子类的print1方法执行了=");
}
public void showMethod(){
print1(); // 子类的
super.print1(); // 父类的
}
}
子类构造器
默认情况下,子类全部构造器的第一行代码都是super(),它会默认调用父类的无参构造器
如果父类存在有参构造器,没有无参构造器,则必须在子类构造器的第一行手写super(...),指定去调用父类的有参构造器(原因是子类构造器默认第一行调用父类的无参构造器,但是父类只提供了有参构造器,Java则不再提供默认的无参构造器,所以子类的第一行必须显示地调用父类的有参构造器)
package com.itheima.d14_extends_constructor;
class F{
// public F(){
// System.out.println("===父类F的 无参数构造器 执行了===");
// }
public F(String name, int age){
}
}
class Z extends F{
public Z(){
// super(); // 默认存在的
super("播妞", 17);
System.out.println("===子类Z的 无参数构造器 执行了===");
}
public Z(String name){
// super(); // 默认存在的
super("播妞", 17);
System.out.println("===子类Z的 有参数构造器 执行了===");
}
}
public class Test {
public static void main(String[] args) {
// 目标:先认识子类构造器的特点,再掌握这个特点的常见应用场景。
Z z = new Z();
Z z2 = new Z("播妞");
}
}
子类构造器可以通过调用父类构造器,把对象中包含父类这部分的数据先初始化赋值,再回来把对象里包含子类这部分的数据也进行初始化赋值。
this()调用兄弟构造器
this和super总结
访问本类成员:
this.成员变量 //访问本类成员变量(如果当前类没有相关的成员变量,则会去父类找)
this.成员方法 //调用本类成员方法
this() //调用本类空参数构造器
this(参数) //调用本类有参数构造器
访问父类成员:
super.成员变量 //访问父类成员变量
super.成员方法 //调用父类成员方法
super() //调用父类空参数构造器
super(参数) //调用父类有参数构造器
注意:this和super访问构造方法,只能用到构造方法的第一句,否则会报错。
多态
什么是多态
多态是在继承、实现情况下的一种现象,表现为:对象多态(父类对象接收子类对象、接口接收实现类实例)、行为多态(子类方法重写、调用父类方法)。
多态的前提:继承、父类引用子类成员、方法重写
多态好处
- 在多态形式下,右边对象是解耦合的,更便于扩展和维护。
- 定义方法时,使用父类类型的形参,可以接收一切子类对象,扩展性更强、更便利。
public static void demo(Person p){
if (p instanceof Teacher){
Teacher t = (Teacher) p;
t.teach();
} else if (p instanceof Student) {
Student s = (Student) p;
s.study();
}
}
多态的问题
因为多态声明的是父类或者接口类型,所以无法访问子类或实现类的特有的方法
类型转换
多态访问成员变量和成员方法
多态是对象和行为的多态,不支持属性的多态
成员变量的访问规则
- 直接通过对象名称访问成员变量:看等号左边是谁,优先用谁,没有则向上找。
- 间接通过成员方法访问成员变量:看该方法属于谁,优先用谁,没有则向上找。
public class Fu /*extends Object*/{
int num = 10;
public void showNum(){
System.out.println(num);
}
}
public class Zi extends Fu {
int num = 20;
int age = 16;
@Override
public void showNum() {
System.out.println(num);
}
}
public class demoMult {
public static void main(String[] args) {
//时用多态写法,父类引用指向子类对象
Fu obj = new Zi();
System.out.println( obj.num); // 10
// System.out.println(obj.age); 错误写法
System.out.println("==================");
obj.showNum(); //没有覆盖重写 方法是父类的 就是10
//子类覆盖重写了就是子类的,就是20
}
成员方法的访问规则
看new的是谁,就优先用谁,没有则向上找。
public class Fu /*extends Object*/{
public void method(){
System.out.println("父类方法");
}
public void methodFu(){
System.out.println("父类特有方法");
}
}
public class Zi extends Fu {
@Override
public void method() {
System.out.println("子类方法");
}
public void methodZi(){
System.out.println("子类特有方法");
}
}
public class demoMult {
public static void main(String[] args) {
Fu obj = new Zi(); //多态
obj.method(); //父子都有,优先用子
obj.methodFu(); //子类没有,父类有,向上找到父类。
// 编译看左边,左边是Fu,Fu当中没有methodZi方法,所以编译报错。
// obj.methodZi(); 错误
((Zi) obj).methodZi();
}
}
对比
成员变量:编译看左边,运行还看左边
成员方法:编译看左边,运行看右边。
标签:Java,String,int,System,001,println,public,out From: https://www.cnblogs.com/kk-koala/p/18328609