小试牛刀:
1.
char a = 20000;
int b = '我';
这个有错误么?
答:没有
当给char赋整数时,需要在0~65535。十进制、八进制、十六进制均可(只能放单个字符),他会对应字符编码表转换成相应的字符
①ASCII码中,一个英文字母(不分大小写)占一个字节的空间
②UTF-8编码中,一个英文字符等于一个字节,一个中文(含繁体)等于三个字节。
③Unicode编码中,一个英文等于两个字节,一个中文(含繁体)等于两个字节。
当给int赋值一个字符时,他会对应字符编码表转换成整数
2.
String类中replace方法和replaceAll方法有什么区别?
replace方法:支持字符和字符串的替换。
replaceAll方法:基于正则表达式的字符串替换。
3.
Object类中有hashCode方法,什么时候需要重写该方法?
????
为什么在编程之前都需要搭建环境,到底在做什么?
将高级语言编译成计算机能读懂的语言
为什么要配置环境变量?
2、Java最大的特性是跨平台,什么是跨平台,为什么能实现跨平台?
平台:操作系统 和 CPU架构
一次编译后生成的目标文件(.class字节码),可以直接在多个平台上运行。
java:一次编译,到处运行
c等其他语言:到处编译,才能运行
.class字节码文件,不直接在机器上运行,而是在jvm虚拟机上运行。
通过jvm,隔离平台指令集的差异。
3、JDK、JRE、JVM都是什么,有什么关系?
JVM :英文名称(Java Virtual Machine),就是我们耳熟能详的 Java 虚拟机。它只认识 xxx.class 这种类型的文件,它能够将 class 文件中的字节码指令进行识别并调用操作系统向上的 API 完成动作。所以说,jvm 是 Java 能够跨平台的核心,
JRE :英文名称(Java Runtime Environment),我们叫它:Java 运行时环境。它主要包含两个部分,jvm 的标准实现和 Java 的一些基本类库。它相对于 jvm 来说,多出来的是一部分的 Java 类库。
JDK :英文名称(Java Development Kit),Java 开发工具包。jdk 是整个 Java 开发的核心,它集成了 jre 和一些好用的小工具。例如:javac.exe,java.exe,jar.exe 等。
显然,这三者的关系是:一层层的嵌套关系。JDK>JRE>JVM。
Java 为什么能跨平台,实现一次编写,多处运行?
Java 能够跨平台运行的核心在于 JVM 。不是 Java 能够跨平台,而是它的 jvm 能够跨平台。我们知道,不同的操作系统向上的 API 肯定是不同的,那么如果我们想要写一段代码调用系统的声音设备,就需要针对不同系统的 API 写出不同的代码来完成动作。
而 Java 引入了字节码的概念,jvm 只能认识字节码,并将它们解释到系统的 API 调用。针对不同的系统有不同的 jvm 实现,有 Linux 版本的 jvm 实现,也有 Windows 版本的 jvm 实现,但是同一段代码在编译后的字节码是一样的。引用上面的例子,在 Java API 层面,我们调用系统声音设备的代码是唯一的,和系统无关,编译生成的字节码也是唯一的。但是同一段字节码,在不同的 jvm 实现上会映射到不同系统的 API 调用,从而实现代码的不加修改即可跨平台运行
变量
1.变量先赋值(初始化),再使用
首先,任何变量在C语言中不赋值的话,会是乱码的形式,可能有安全问题。所以java修正了这点,对于局部变量强制让你赋值。至于为什么成员变量有自己的初始值,而不需要强制赋值,那是因为new对象的时候,构造函数帮你初始化了
2. java变量的作用域:包含它的最近的一对大括号
3. 变量的命名规则:
int b3 = 0;
int 3b = 0;
int 我 =0;
int $我=0;
int _我=0;
int ¥我=0;
第几个是错误的?
只有第二个
由数字、字母、下划线、$,以及各国语言文字组成,但是不能以数字开头,不能与关键字或符号冲突
数据类型
byte b = (byte)128;
System.out.println(b);
结果是什么?
答:-128,多出来的就从该类型的最小开始重新算,多出几个加几个
byte 1字节 8位 -2^7 ~2^7-1 -128~127
char/short 2字节 16位 -2^15 ~ 2^15-1 -32768~32767
int 4字节 32位 -2^31 ~ 2^31-1
128的二进制 原码为000…00010000000,前面有好多0,加上后面的一共32位
-128的原码为10…00010000000
补码为011…111101111111+1=111…1111000000,强转为byte后为10000000,所以为-128
一般系统默认就会用4字节(int)存储数字
int a=2147483647;
int a2=2147483648;
long a3=2147483648; 应改为long a3= 2147483648L;
a2,a3都会报错,因为a2超出了范围,a3应该加L
float是4字节,long是8字节 ,为什么float表示的数的范围比较大?
答:因为float的表示方式使用的科学计数法来表示的,虽然字节少,但是存储的数比较大
自动转换,强制转换:
1.基础数据类型,数值之间
2.子类父类
3.包装类和基础数据类型
byte---------->>double
低-------------->>高
低到高,自动转换,无风险。高到低,强制转换,但是可能会数据丢失
char a=20000;
c=a+1;//报错
c=20000+1不报错
迷惑点:为什么这里给char赋值20000时他不会报错,赋整数时默认的不是int么?,这里不应该强制转换才行么??
运算符
&&:他会有短路情况
int a=1;
int b=2;
Boolean a1=b<a&& a++>1;
System.out.println(a1);
System.out.println(a);
输出结果:false
1
这里的a 不是2的原因:&&是一种短路运算,当前面错误时他就不会进行运算了,,后面的a++就不会执行了但是&运算他会执行
同理||会短路,|不会短路
位运算:&,|,^
int a1=5&2;0
int a2=5|2;7
int a3=5^2;
写出他们的二进制形式5:101 ;2:010,
&运算,两个都为1才是1,不同取0,所以为000
|运算,两个有一个为1就为1,所以为111
^运算,两个相同为1,不同为0,所以为111
请使用三种方式定义长度为5的整形数组;
int [] a=new int[5];
int []aa={1,2,3,4,5};
int []aaa=new int[]{1,2,3,4,5};
为什么数组定义的变量不可以直接存储在栈中,而是使用堆和栈相结合的方式,
因为栈的特点是读取速度快,但是存储量少,而堆的特点是读取速度慢,但是空间量大。如果将数据直接存储在变量中,变量在栈中,很快就会导致栈空间满,溢出,程序崩溃。
理解引用数据类型:
这里sa1和sb1用的是同一个地址,所以,改sb1的值就相当于改了sa1的值
数组的扩容:
手动
Scanner sc=new Scanner(System.in);
int[] cj=new int[5];
int count=0;
System.out.println("请输入学生的成绩");
while(true) {
int c=sc.nextInt();
if(c==-1) {
break;
}
if(count==cj.length) {
int [] nw=new int[cj.length*2];//重新创建一个2倍cj长度的数组
for(int i=0;i<cj.length;i++) {
nw[i]=cj[i];
}
cj=nw;
}
自动扩容
cj=Arrays.copyOf(cj, cj.length*2);
自动排序Arrays.sort(cj))
数组的拷贝,因为数组的扩容会使得没有赋值的空间的元素为0,拷贝可以使得这种状态消失,
int [] tmp=new int[count];
Systrm.arraycopy(cj,0,tmp,0,count);
交换两数据的值:
这种的是交换不了值的:他只改变的是形参的值
public static void fn1(int a,int b)
{
int t=a;
a=b;
b=t;
}
这种是可以交换值的:引用数据类型,传参是地址值,形参和 和实参操作同一 内存空间,
public static void fn2(int[] a,int [] b)
{
int t=sa[0];
sa[0]=sb[0];
sb[0]=t
}
!!!!!!!!这里他没有交换数据,所以他不会改变值
public static void fn3(int[] a,int[]b)
{
int[] t=si;
si=sj;
sj=t
}
字符串常见操作
达到熟练、灵活应用的程度,包括
遍历 charAt、长度length、截取substring、分割split、替换replace、
查找indexOf、比较 equals compareTo、大小写 toUpperCase等
截取路径中文件名:
path = "D:\Soft\OracleXE 11g\DISK11\setup.exe";
当把这行粘贴到eclipse中,它会变成双斜杠,因为/有特殊含义,可以和一些字母产生作用,所以需要双斜杠进行转义
//获取path中文件名(setup)
int i1 = path.lastIndexOf("\");
int i2 = path.lastIndexOf(".");
//包前不包后
String name = path.substring(i1+1, i2);
System.out.println(name);
replace和replaceAll区别
path = "D:/Soft/OracleXE 11g/DISK11/setup.exe";
//replaceAll(正则 regex)和replace(普通)
System.out.println(path.replace("/", ""));
System.out.println(path.replaceAll("/", ""));
System.out.println("----------------------");
String path2 = "D:|Soft|OracleXE 11g|DISK11|setup.exe";
System.out.println(path2.replace("|", ""));//去掉|
// | 在正则中表示或
System.out.println(path2.replaceAll("|", “”));//没有影响
System.out.println(path2.replaceAll(“S|s”, “”));//去掉大S 或 小s
String[] sa = path2.split("|");//split也支持正则表达式
System.out.println(Arrays.toString(sa));//每个字符单独成串
System.out.println(Arrays.toString(path2.split("")));等同于第一行的那个
String[] sa2 = path2.split("\\|"); //按照竖线|分割字符串
字符串在进行存储、网络传输时,都需要转换成字节序列,使用getBytes。
byte[] ba = "你好".getBytes();//gbk
System.out.println(Arrays.toString(ba));
以上代码执行结果为,[-60, -29, -70, -61]
字符串转换为字节数组,getBytes()默认采用平台编码,目前为gbk编码。
我们也可以采用utf-8编码完成转换:
byte[] ba1 = "你好".getBytes("utf-8");
System.out.println(Arrays.toString(ba1));
执行结果:[-28, -67, -96, -27, -91, -67]
gbk 、 utf-8 常见的两种编码方式
gbk :英文字符1,其他(中日韩文字符号)2字节
utf-8 : 英文字符1,中文3字节,其他不定1~6
通过new String也可以将字节数组byte[] 转回字符串:
byte[] bc = {-56,-70};
String st = new String(bc,"gbk");
System.out.println(st);//群
byte[] bd = {-27, -91, -67};
String st1 = new String(bd,"utf-8");
System.out.println(st1);//好
各种编码方式介绍
Unicode是一个码表,其中记录着 字符《--------》数字对应,目前收录14万+个字符!
在计算机中表示14万个字符,得有14万个数字与其对应:
1字节 256个 不够
2字节 65536个 还不够
4字节 很多个 够了
为了完整描述14个字符,需要使用4个字节表示,根据此产生了utf-32编码。
utf-32:
定长的4字节编码,完全参照Unicode码表中的值,位数不够,就补0
优点:非常直接,与Unicode码表中的值相同。
缺点:浪费空间,即使表示一个简单的汉字,或英文字符,也需要4个字节。
utf-8:
非常完美的内部的算法
变长,1~6字节不等,英文字符符号占1个字节;汉字占3个字节。特殊字符4个字节
优点:utf-8很完美的解决了空间问题
缺点:1~6字节变长,在内存中运算时对不齐,不方便运算。
utf-16:
“变长” / “定长”:常见的字符2字节,不常见字符4字节。
如果字符在Unicode码表中的值<=两字节,编码结果就是固定的两个字节的码表值。前面会有两个前导字符???
对于不常用的特殊字符,例如emoji,超出两个字节;utf-16也有内部的算法,编码结果为4字节。
utf-16就是一种折中方案,既能保证数据对齐,又不太浪费空间
综上:
Unicode只能是一个码表,而不是一种编码方式。
根据Unicode码表中的码值,衍生出了utf-8\16\32三种编码方式,每种编码方式有自己内部的算法。
GBK码表,能否说成GBK编码方式,是否也对应了很多编码方式呢?
gbk码表中,一共有2w+个字符,中文占2个字节。英文1个字节
即不存在浪费空间的问题,又不存在对不齐的问题。
所以,gbk码表 即为 gbk编码,就是按照码表值 直接得到 字节数组。
在jvm内存中使用什么编码方式?
public static void main(String[] args) {
String s = "你好";//代码运行时,jvm如何在内存中存储“你好”这两个字
//猜
//String ----> char[] -----> char
char c = '你'; //char字符,在内存中的存储形式
System.out.println((int)c);
System.out.println(Integer.toHexString(c));// 4f60
//两个字节:gbk 或 utf-16
//gbk C4E3 排除
//utf-16 4f60 OK
}
总结:
jvm存储字符时,使用utf-16编码
因为utf-16,2字节或4字节,可以在内存中对齐,方便运算;并且不浪费(太多)空间。
字符串在进行存储、网络传输时,不讲究对齐,越小越好,更多采用utf-8 / gbk
codePointAt与charAt:
charAt(n):返回值char,表示char[]中的第n个值,如果字符串中有特殊字符,使用chatAt无法获取字符。
codePointAt(n):返回值为int,可以返回完整的码表中的字符值。应对有特殊字符的场景
字符串比较:equals和compareTo
equals:
比较两个字符串内容是否相等(重写过后的,没有重写的equals和==是一样的。比较的都是地址)
compareTo:
返回char的差值,或长度的差值(当两个相同时,返回0。当第一个是第二个的一部分时,返回他们的差值)
字符串常量池:
1.使用自变量方式定义字符串时,将定义的字符串放入常量池(一块独立的内存空间),而不是堆;(如String a = “123”
2.当再使用字面量定义字符串时,先从常量池中找,有没有该字符串,如果有就直接使用,不会新建。
如果使用new String的方式创建字符串对象,跟常量池无关,始终创建新对象
String a =new String(“123”)
字符串不可变性:
字符串在定义后,其在内存中的值是不可变的;当改变字符串内容时,其实是又创建了一个新的字符串。
StringBuffer:
字符串缓存,字符串操作类
不好的做法:
String a="";
for(int i=0;i<100;i++){
a=a+i;
}
System.out.println(a)
这样中间产生了太多的垃圾空间(GC垃圾回收机制)
优化后:
StringBuffer sb=new StringBuffer();
for(int i=0;i<100;i++) {
sb.append(i);
}
System.out.println(sb);
如果要对字符串进行频繁的操作时,推荐使用StringBuffer
StringBuffer中还包含了许多字符串的操作方法 ,
sb.reverse 反序
类与对象
理解类的作用,为什么要定义类?
总结:
类是一种引用数据类型
类是一种用户创建的数据类型(每定义一个类,就相当于创建了一个数据类型)
类中可以包含多个数据,作为一个整体
类中可以包含多个功能,操作数据
类可以像普通数据类型那样定义变量、作为参数、返回类型、
要求1:
只有在创建矩形时,设置宽高;创建之后,不能修改宽度和高度。
构造方法+private+this:
要求2:
当使用System.out.print(矩形)打印矩形对象时,输出该矩形的宽度和高度。
Object类+重写toString:
要求3:
如果将width和height改为static静态的,是否可以?
静态表示独一份:
静态属性,所有的对象共用同一个属性,也称为类属性
静态方法,所有的对象共用同一个方法,也称为类方法
普通的属性/方法,每个对象中均有自己的属性和方法,对象之间互不影响;
普通属性和方法,称为对象属性和对象方法。
要求4:
能否将print、compareTo方法直接改为静态的?
不可以!
规则:静态方法中不能访问普通属性
反过来:
参照上图,在普通方法中,能否使用静态属性?
完全可以!!
要求5:
Rect类中定义一个静态的print方法,来打印矩形对象的形状,该如何定义?
如果想要将一个方法定义成静态方法,该方法中不能依赖具体对象,
即不能直接使用类中的普通属性和普通方法;
继承
继承的作用
1)代码的复用
2)类与类之间建立联系
重点内容
继承中的构造方法
继承的内存模型
重写与重载
Object根父类
1)构造方法与内存模型
问题1:
创建Dog对象时,有没有调用Animal的构造方法?
有!
间接的说明,创建了Animal类对象
问题2:
有没有调用Dog的构造方法,与Animal构造,谁先谁后?
有!
先调用Animal构造(创建animal对象),再调用Dog构造(创建dog)
创建了两个对象,分别是Animal对象和Dog对象。
继承后,子类对象的内存模型(子对象中包含父对象):
如果父类没有无参构造,那么系统就无法自动完成调用父类无参构造,来创建父类对象。
既然完不成自动调用,需要程序员显式的手动调用父类有参构造,创建父类对象。
在子类构造方法中的第一行,通过super关键字。
方法的重写
父类中的方法,不满足子类的要求。子类可以定义一个与父类 “完全一样” 的方法,来覆盖父类方法。
Animal中的shout:
Dog重写shout:
问:
1、子类shout的public去掉是否可以?
不可以!
重写时,不能减少修饰符的可见范围。public ,default,private,protect
2、父类shout的public去掉是否可以?
可以!
3、将子类的shout添加一个参数,是否可以
可以!
但是此处不再是重写,子类方法不能覆盖父类方法。
是重载,满足两个方法方法名相同,参数不同。
4、修改子类的shout返回类型,是否可以?
不可以!
既不是重载,(因为方法名和参数,均相同)重载和返回值类型无关,如有两个函数分别为int f(),void f(),当调用其中的两个时,会不知道调用的哪个
又不是重写,(返回类型一个是int,一个是void,覆盖不了)
Object类
Object类是所有类的根父类。
所有的类都继承了Object,获取Object类中的方法。
equals
toString
hashCode (集合)
notify\notifyAll (多线程)
wait (多线程)
getClass (反射)
1、toString
打印对象时,自动调用toString方法,获取值。程序员可以根据需要,重写toString.
2、equals
其实在Object类的实现中,equals方法就相当于 == 比较,比较两个对象的地址是否相同。
字符串的equals和 == 就不相同,是因为,字符串重写了equals的比较逻辑,实现内容的比较。
对于我们的类,也可以根据需要重写equals,自定义比较逻辑。可以采用手动重写,或自动生成两种方式,
理解多态
1(规则) 父类的引用,可以指向子类的对象
Animal a = new Dog();
Animal b = new Cat();
2(规则) 通过a调用方法时,会调用子类重写后的方法
a.shout() ----> Dog中的shout
b.shout() ----> Cat中的shout
3(理解) 运行时多态
an为Animal类型的数据,an.shout到底会执行哪个方法,只有在运行时才能确定。
void fn (Animal an){
an.shout(); //只有在真正运行时,才知道an指向谁,调用的哪个方法
}
打破了强数据类型的限制,大大增加了编程的灵活性,
fn的参数可以是Animal,也可以是Dog、Cat、Monkey等Animal的子类。
4(理解) 面向对象的角度理解
现实世界:
狗(花花)是一个狗
狗(花花)是一个动物
面向对象:
Dog d = new Dog(“花花”);
Animal an = new Dog(“花花”);
错误:
现实: 动物是一个狗
面向对象中: Dog d = new Animal();
多态的弊端:
父类引用,指向子类对象;会隐藏子类对象中的新增的方法。
如果需要调用新增方法,那么进行强制转换,将父类类型转换为子类类型。
强制转换,都是有隐患的!!!!,需要通过 instanceof 进行判断,是否属于该类型。
接口 interface
为什么要有接口
多态是面向对象三大特性的核心,利用多态特性我们可以写出非常灵活的代码。
但是多态的前提是,类与类之间要有关系,父子关系。
1通过继承可以产生关系。
但是继承不能随便使用,必须满足is a原则。
鸭子Duck,飞机Plane ----》如何产生关系?
共同的特征(功能)—》飞
将多个功能相同的类,
2 通过接口产生关系。
现实世界:
飞机能飞
鸭子也能飞
程序实现:
interface Fly
class Duck implements Fly
class Plane implements Fly
鸭子和飞机有共同的父接口 Fly
所以,通过接口,我们可以更广泛的建立类之间的关系。
即,只要是具有相同功能的类,都可以提取出一个共同的父接口,就可以使用多态特性。
集合框架
集合类《-----》数组
局限性:
1)指明大小
2)手动扩容
3)手动遍历
各种各样的集合类,代替数组,存放数据。
整体结构
Collection接口 (顶层)
集合的增删(改)查等功能
List(序列)、Set(集)、Queue(队列) 子接口
各种各样的实现类,ArrayList ,HashSet 等。
为什么这样设计?
不同的集合,有不同的特征,为了满足各种场景需求。
Collection接口中定义了,集合的基本功能。
List(序列)、Set(集)、Queue(队列)根据自身特征,新增了一些功能。
List 列表
特征:有序,可重复
存储顺序和插入顺序是一致的。
List在Collection接口的基础上,添加了一些关于元素顺序(与索引index有关)的方法。
List接口的实现类:
为了满足不同场景需求,提供了很多实现类。其中最常用的是ArrayList。
List的变量删除陷阱
for(int i=0;i<ls.size();i++) {
String s=ls.get(i);
if(s==null)
ls.remove(i);
i--;//必须加上这一步
}
该变量会被删除,其后的变量会补上来,所以得i - - 再次检测该位置的变量,不然如果有两个连续满足条件的变量会忽略一个;
或者直接使用
while(ls.contains(null)) {
ls.remove(null);
}
涉及到比较相等使用的是对象的equals方法
使用indexOf方法差找不到
List<Student> sts=new ArrayList<>();
sts.add(new Student("张三",10));
sts.add(new Student("张2",10));
sts.add(new Student("张4",10));
sts.add(new Student("张5",10));
sts.add(new Student("张6",10));
System.out.println(sts);
int a=sts.indexOf(new Student("张三",10));
System.out.println(a);//a=-1
但是equals方法要记得重写,他默认比较的值是地址
列表和数组之间的互相转换
集合转数组:有两种,一种是不强调类型,直接转为Object,另一种是直接确定类型
1. Object[] oo=ls.toArray();
for(Object o:oo) {
String s=(String)o;
System.out.println(s);
}
System.out.println(oo);
2. String[] ss=ls.toArray(new String[0]);
System.out.println(ss);
数组转变集合
String[] bs={“1”,“2”,“3”};
list bbs=new ArrayList<>(Arrays.asList(bs))
将代码中的实现类ArrayList,换成任何List接口的其他实现类,代码的运行结果不变各个实现类虽然内部的实现方式不同,但是功能的一样的,都与List中定义的功能一致,所以运行结果一致。
不同实现类的区别:
LinkedList内部通过一个个节点的连接,形成集合。
方便删除、插入等操作
ArrayList 内部通过数组形参集合。
方便查找、访问等操作。
CopyOnWriteArrayList 和 Vector也是List,它们是线程安全的集合,但是它们内部控制线程安全的机制也不同,使用场景不同。
如果集合中数据量比较少(1万条以下),几乎性能没太多差别,如果不涉及到线程安全,通常都使用ArrayList。
面试题: ArrayList是使用数组来实现,那么必然会有扩容问题。
1)内部的扩容机制是什么?
List l = new ArrayList<>();
l.add l.add l.add l.add …
new ArrayList<>():数组长度是0 DEFAULTCAPACITY_EMPTY_ELEMENTDATA {}
第1次add 0------》10
…
第11次add 10------>10+10>>1 = 15
第16次add 15---->15+15>>1 = 22
第1次由0—》10,以后每次扩容均扩大为1.5倍
2)new ArrayList<>(0)是否可以,有什么坏处?
空间初始值为0 EMPTY_ELEMENTDATA {}
第1次add 0----->1
第2次add 1----->2
第3次add 2---->3
第4次add 3---->4
第5次add 4----->6
第6次add 6----->9
…
在最初开始时会频繁扩容。不会由0,直接扩容为10。
Set集
不重复 且 “无序”
Set接口,没有索引/下标这样的概念,也不存在第几个元素这样的说法。
所以,Set接口中没有List接口中定义的有关index的那些方法。
实现类HashSet 、TreeSet等,最常用的就是HashSet
遍历问题
不能使用普通for循环,需要使用增强for,或迭代器(理解内部过程)
注意,在遍历时,不要删除/添加元素,否则报错:
真需要删除时,可以通过迭代器本身的remove方式,删除当前节点:
真正理解HashSet的无序和不重复!认识HashSet 的存储机制:
Set集合就像一栋大楼,有很多层(0~15),每一层有很多房间。
此时,向集合中添加元素 set.add(s),那么该元素s能否入住,以及入住到哪个房间呢?
1 获取s的hashcode值v
2 将该值v对16求余,获取“楼层”x
3 将s与x层中的每个元素通过equals比较
4 如果没有相等的,就将s存入x层的空间,否则舍弃s
关键点:
先hashCode获取值,再equals比较。
如果两个元素hashCode不相等,很可能没有equals比较的机会,就被存入到集合中。
如果想要保证HashSet中的元素真的不重复,那么得保证,equals相等的元素的hashCode值也相等,不能是随机的地址值。
所以,需要重写hashCode方法(eclipse自动生成)。
这种存储方式(hash 散列),除了麻烦,有什么好处?
方便元素查询。
(以空间换时间)
关于TreeSet
TreeSet,自动排序,要求集合中的元素是可比较的。
TreeSet,不重复,但是与hash不同,通过compareTo方法来判断是否相等。
1 可比较
需要Student类,实现Comparable接口,表示可比较。
2 判定重复
如果两个元素通过compareTo方法比较结果为0,表示元素相等,舍弃元素。
Map
Map与Collection什么关系?
没有关系!
Collection 单个元素的集合
Map 键值对的集合
键Key----值Value
Map特征
1)键值对
2)键不能重复
map的使用场景
我需要存储班级所有学生姓名和成绩。【都不重名】
List < Student>
Set < Student>
Map
创建 mp对象
1.查询张三考了多少分?
用get()函数就可以
mp.get("张三");
2.班级里有没有100分?
mp.containsValue(100);
3.将李四的成绩修改为60分
mp.put("李四",60);
4.将Lucy的信息删除
mp.remove("lucy");
5.计算班级的平均分
mp.values();他返回的是Collection类型的,所以要用Collection来接受,
Collection< Integer> c=mp.values();
6.打印不及格的学生姓名
首先获取set集合
Set< String >names= mp.keySet()//键的结合
for(String name;names){
int sc=mp.get(name)
if(sc<60)
System.out,println(name+"不及格");
}
7.遍历map;
Set < Entry< String,Integer>> es=mp.entrySet()//entry是键值对,set中放的是键值对集合
for(Entry< String ,Integer >e:es){
System.out,println(e,getKey());
System.out,println(e.getKey());
}
List、Set存储学生对象,完成以上操作都需要遍历集合,很烦。
Map来存储,不仅不需要创建学生对象,直接调用Map中的方法就能完成功能。
为什么可以通过键来获取值,但不能通过值获取键?
答:1个键对应一个值,一个值可能对应多个键
为什么获取值的集合是Collection ,获取键的集合是Set
答:因为Set是不允许有重复的
HashSet和HashMap有很多类似的地方。他们在实现时有什么关系?
HashSet依赖HashMap,其内部是使用HashMap存储数据的
向Set中存元素时,该元素作为Map的key存入,而值的value是一个固定的Object对象
1) 认识IO流
流:
数据流,数据从一个地方移动到另外一个地方,即数据传输
上网、文件读写、蓝牙等等各种场景
凡是涉及到数据传输,底层都需要使用IO流。
对于Java编程来说,涉及到IO有两个场景:1)文件读写 2)web数据【JavaWeb】
I:input 输入
O:output 输出
Stream 流
输入输出流。
读文件是属于输入流还是输出流? 输入 InputStream
写文件是属于输入流还是输出流? 输出 OutputStream
2)读文件
字节流读取
File f=new File("C:\\Users\\ASUS\\Desktop\\aaa.txt");
InputStream ins=new FileInputStream(f);
byte[] a =new byte[100];
int count=0;
while(true){
int b=ins.read();
if(b==-1){
break;
}
a[count++]=(byte)b;
}
String c=new String(a,"utf-8");
System.out.println(c);
ins.close();
理解读取到的内容
byte[]转String、与乱码
字节读取文本文件,步骤繁琐易错,并且读到字节序列还需要手动转换为字符串。
所以很少使用字节流操作文本文件。
字符流读取
//创建File对象,关联某路径
File f=new File("C:\\Users\\ASUS\\Desktop\\aaa.txt");
//创建文件流(字节流)
InputStream ins=new FileInputStream(f);
//继续封装,生成字符流
InputStreamReader insr=new InputStreamReader(ins);
char a[]=new char[1000];
int count=0;
while(true){
int b=insr.read(a,count,100);
if(b==-1){
break;
}
count+=b;
if(count+100>a.length){
a= Arrays.copyOf(a,a.length*2);
}
}
String c=new String(a,0,count);
System.out.println(c);
insr.close();
ins.close();
基础字符流读取文本文件,读到的内容是char或char[],不是最终的字符串,这中间的转换也很烦。所以,我们也不常用这个!
BufferedReader,可以整行读取,直接得到字符串。
File f=new File("C:\\Users\\ASUS\\Desktop\\aaa.txt");
InputStream ins=new FileInputStream(f);
InputStreamReader insr=new InputStreamReader(ins);
BufferedReader br=new BufferedReader(insr);
System.out.println(br.readLine());
while (true){
String s=br.readLine();
if(s==null)
break;
System.out.println(s);
}
br.close();
insr.close();
ins.close();
3)写文件
写文件时必须保证文件存在
字节流写
追加模式,在追加时一定要注意与原有编码格式一致!!!
File f=new File("C:\\Users\\ASUS\\Desktop\\aaa.txt")
if(!f.exists())
f.creatNewFile();
OutputStream ins =new FileOutputStream(f);
ins.write(-26);
ins.write(-120);
ins.write(-111);
ins.close();
他是以字节的方式进行写入
一般不用
字符流
File f=new File("C:\\Users\\ASUS\\Desktop\\aaa.txt")
if(!f.exists())
f.creatNewFile();
OutputStream os =new FileOutputStream(f);
os.write(-26);
os.write(-120);
os.write(-111);
os.close();
字符流写
默认将原内容清除,从头写
File f=new File("C:\\Users\\ASUS\\Desktop\\aaa.txt")
if(!f.exists())
f.creatNewFile();
OutputStream os =new FileOutputStream(f,true);在这里加上true就会追加模式写文件
OutputStreamWriter wr=new OutputStreamWriter(os)
wr.writer('我');
wr.writer("我爱中国");
wr.writer(0x1543);
wr.close();
os.close();
字符流可以写入char,char[],String 使用起来较方便
PrintWriter最方便
PrintWriter p= new PrintWriter("C:\\Users\\ASUS\\Desktop\\悯农.txt");
p.println("锄禾日当午");
p.println("汗滴禾下锄");
p.close();
这样就可以写入,最方便,
若想要追加,还是用第二种方法,或者使用
File f=new File("C:\\Users\\ASUS\\Desktop\\aaa.txt")
if(!f.exists())
f.creatNewFile();
OutputStream os =new FileOutputStream(f,true);在这里加上true就会追加模式写文件
OutputStreamWriter wr=new OutputStreamWriter(os)
PrintWriter p= new PrintWriter(wr)
p.println("锄禾日当午");
p.println("汗滴禾下锄");
p.close();
wr.close();
os.close();
多线程
进程
进行中的程序、运行中的程序
计算机中打开的每个应用程序,可以认为是一个进程
线程
依附于进程,进程中某个单独的功能模块
一个进程可以包含一个或多个线程,同时运行,执行多个功能
例如:
钉钉:既可以同步观看直播,又可以同时刷聊天。
Eclipse:既可以输入内容,又可以同步编译。
为什么需要多线程
1)功能需要(用户体验)
2)执行效率
理解线程调度:
为什么这么多的线程和进程可以同时运行(并发)?
4核 8核 CPU------》最多同时运行4、8程序
操作系统的调度:
时间片轮转
n多个进程/线程等待执行---------》取出某一个线程,分配很短的时间片(2ms)执行------》取下一个,再执行-----》
1s,调度500次,好像是同时执行的一样。
2 基础使用
线程------面向对象----》Thread类
创建线程:Thread t1 = new Thread();
启动线程:t1.start();,启动线程后会执行Thread中的run方法!
执行t1中的run方法,默认target==null,所以没有任何逻辑!
1)创建有意义的线程
方法1:
创建子类,继承Thread,重写run方法
public class MyThread extends Thread{
@Override
public void run() {
for(int i=1;i<=100;i++) {
System.out.println(i);
}
}
}
方法2:
创建子类,实现Runnable接口
public class MyRunnable implements Runnable{
@Override
public void run() {
for(int i=1;i<=10000;i++) {
System.out.println(i);
}
}
}
target是Runnable类型的变量,
2)start 与 run
Thread t1 = new MyThread();
Thread t2 = new MyThread();
t1.start();//t1.run()
t2.start();//t1.run()
如果把第3和4行的调用start变为调用run,结果和意义将大不相同。
我们知道多线程的执行机制是cpu轮循调度,只有start才会将该线程加入到cpu的调度池!,于是我们看到t1和t2交替运行。
而run只是普通的方法调用,它会先执行t1.run,打印1100;然后执行t2.run,再打印1100,没有交替,没有调度,顺序执行。
3)Thread类常用方法
sleep ,睡眠方法,可以让线程中止运行
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
setName/getName,设置线程名
Thread t1 = new Thread(r1);
t1.setName("窗口1");//设置线程名
Thread t2 = new Thread(r1);
t2.setName("窗口2");
public void run() {
for(int i=1;i<=10000;i++) {
String name = Thread.currentThread().getName();//获取当前线程名
System.out.println(i);
}
}
join,线程合并,主线程等待子线程执行结束
Thread t1 = new MyThread();//创建线程
t1.start();//启动线程
t1.join();//主线程等待
System.out.println("打印完毕!");//t1执行完毕后,输出该行
3 多线程并发
多线程如果操作了同一个“数据”,就会出现线程并发问题
1)静态与动态
MyRunnable:
public class MyRunnable implements Runnable{
@Override
public void run() {
for(int i=1;i<=10000;i++) {
String name = Thread.currentThread().getName();//当前线程的名字
System.out.println(name+"卖出第"+i+"张票");
}
}
}
线程:
此时t1和t2是否会出现线程并发问题呢????
t1和t2均是基于MyRunnable对象r1构建的,当线程启动后,会同时执行r1的run方法!
代码是静态的,而程序(线程)是动态的!
我们分析的是动态运行过程:
当线程启动后,各种执行run方法,在自己的空间中定义局部变量i,互不影响!
每个线程都有自己的空间,来存储在程序运行过程中定义的变量,线程之间互不影响。
2)局部变量与全局变量
修改MyRunnable,添加全局变量。
此时再启动t1,t2线程,两个线程会同时操作同一个count,并发问题就出现了。
count为全局变量,Myrunnable r1对象的属性,不是线程运行过程中创建的局部变量!
t1,t2在运行过程中使用的是同一个count,于是就出现了并发错误。
3)并发问题产生原因
为什么t1,t2,t3交替进行count++就会出现数据错误呢??
关键点:
1)java语句并不是原子操作,count++会转换为n条计算指令执行。
2)cpu调度是随机的,在指令运行的任何位置都会被调度走,再调回时就很容易产生数据错乱。
4)sychronized同步代码块
通过sychronized同步代码块解决并发:
作用:保证某一时刻只有一个线程执行同步代码块中的内容,其他线程需要等
机制:sychronized让线程去竞争括号中对象的锁,获取到对象锁的线程,可以进入到代码块中执行;没获取就在外部等!
当代码执行完毕后,获取锁的线程会归还锁。
对象锁:java中每个对象都有一把锁,这个锁只有在synchronized关键字这里才发挥了作用。
但是sychronized里也都不一定是this
4 加深理解并发和锁(这里没学懂)
Runnable:
该Runnable的作用是向某集合中添加10000个元素。
测试类:
public static void main(String[] args) throws InterruptedException {
List<String> ls = new ArrayList<>();
Runnable r1 = new ListRun(ls);
Thread t1 = new Thread(r1);
Runnable r2 = new ListRun(ls);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
t1.join();
t2.join();
//期望值是20000
System.out.println(ls.size());
}
在测试类中,我们创建了两个Runnable ,r1和r2;然后基于它们创建了两个线程t1和t2。
问题1:
t1和t2是否构成线程并发问题?
t1和t2好像依赖不同的对象r1和r2,但是r1和r2中使用的是同一个列表ls!
所以t1和t2同时操作集合ls,构成线程并发!
问题2:
main方法执行是否报错?
报错,并且执行结果也不对!
这是说明ArrayList这个集合是非线程安全的!
问题3:
通过synchronized (this)同步代码块,能否解决问题?
不能,依然报错!
synchronized (this)括号中this表示当前对象,而t1线程依赖的当前对象是r1,t2依赖r2,
所以t1和t2在动态执行代码时,对this解析的对象不是同一个,这就构不成竞争关系了!
我们应该在括号中写唯一的对象,例如 synchronized (ls)、synchronized (ListRun.class)
问题4:
线程安全的集合类有哪些?
java.util.concurrent包下有很多线程安全的List、Set、Map、Queue等集合,
在使用这些集合时,我们可以不用考虑手动控制并发。