1.面向对象基础
1.1 方法
1.1.1 方法重载:
- 同一个类中,多个方法的方法名相同,参数列表不同,根据调用方法时的传参确定具体调用的哪个方法,构成方法重载。
1.1.2 方法重写:
- 在子类继承父类之后,对于父类中已有的方法(原方法不满足功能时)可以进行重写(覆盖),重写后的方法必须与父类中的方法的方法名、返回值类型、参数类型保持一致,只可以改变方法中的逻辑代码
1.2 类和对象
1.2.1 面向对象和面向过程
面向对象:虚拟世界模拟现实生活
面向过程:按照顺序逐步完成
1.2.2 类与对象关系:抽象与具体
- 类: 同一类事物抽象描述
- 对象:具体实例
- 属性/成员变量:对象的特征
- 方法:对象所做的事情
具有相同属性和方法的对象组合成一类。比如定义实体类的时候,set/get
- 对象的内存分析
一次编写类、属性、方法、测试类(含有main方法的类,然后实例化一个Student类的对象。
步骤: main方法进栈,测试类StudentTest在方法区,执行new一个对象zs,这个对象存在堆中,栈中存放这个对象的地址。
- java内存划分
1.2.3 成员变量和局部变量
- 定义位置不同
- 作用范围不同
- 默认值不同(局部变量没有默认值)
1.2.4 封装
- 把一段处理逻辑的代码封装成一个方法,调用时只需注意输入和输出,不用管如何实现
- private加在实体类的对象属性前面,在该类外部,属性不能直接"."得到,必须使用setter/getter方法获取,可以在setter方法中写if判断,使其赋值合理,比如年龄不能赋值为负数
1.3 继承
1.3.1 参数传递
传递的参数为:
- 基础数据类型:每次获取参数都是本身的值,改变之后不会对其他方法的引用产生影响
- 引用数据类型:传递的只是这个参数的地址,改变属性值之后,其他方法引用时,属性值也发生改变,会有影响
1.3.2 static关键字
- 修饰成员变量:经过static修饰过的静态属性,被存在方法区中,被堆中的对象共享。
- 好处:只需要开辟一片空间
static String name;
Student s1 = new Student();
Student s2 = new Student();
//s1、s2中的 name 是相同的,都是取的方法区中的
String name;
Student s1 = new Student();
Student s2 = new Student();
//s1、s2中的 name 不一定相同,取堆中相应对象中的属性值
1.3.3 继承
- 子类继承父类的属性和方法,然后再写自己独有的属性和方法。
- 减少代码的冗余性
- class A extends B
- 子类不能继承父类的构造方法
- new一个对象时,可以使用无参构造方法,然后setter给属性赋值(常用);也可以使用有参构造方法赋值(少用)
- 例子:
实体类:
public class Exer01 {
String name;
int age;
//无参构造
public Exer01() {
}
//有参构造
public Exer01(String name, int age) {
this.name = name;
this.age = age;
}
//setter/getter
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
//重写toString()方法---方便打印出对象详细内容,而不是只显示一个地址
public String toString(){
return "name:" + this.name + " age:" + this.age;
}
}
测试类:
public class ExerTest01 {
public static void main(String[] args) {
Exer01 e1 = new Exer01("xiaoming",18); //使用有参构造方法赋值
e1.setAge(19);
System.out.println(e1.toString()); //name:xiaoming age:19
Exer01 e2 = new Exer01();
e2.setName("xiaoguang");
e2.setAge(30);
System.out.println(e2.toString()); //name:xiaoguang age:30
}
}
1.3.4 this 和 super
- this可以访问本类中的属性和方法,也可以访问其父类中的属性和方法
- super只能在子类中使用,用来访问其父类中的属性和方法
1.3.5 实例化子类时,父类做了什么
- 先执行父类的构造方法,再执行子类相匹配的构造方法
1.子类构造方法中没有指定,先执行父类无参构造方法
2.子类构造方法中指定了,先执行父类相应的构造方法
子类、父类中构造方法的执行顺序
1.4 多态
1.4.1 定义
Person p = new Student();
1.父类引用指向子类对象
2. 优先调用子类重写以后的方法(子类必须重写父类方法)
3.方法调用总是作用于运行期对象的实际类型,允许添加更多类型的子类实现功能扩展,却不需要修改基于父类的代码。
1.4.2 好处
减少代码冗余性
1.4.3 条件
- 子类继承父类
- 子类重写父类方法
- 父类引用指向子类对象
1.4.4 多态中两种类型转换
- 向上转型–自动类型转换
//Dog转Animal
Animal animal = new Dog();
//或者
Dog dog = new Dog();
Animal animal = dog;
- 向下转型–强制类型转换
- 先使用instanceof关键字判断
Animal animal = new Dog();
//Animal animal = new Cat();
if(animal instanceof Dog){
Dog dog = (Dog)animal
dog.play();
}
if(animal instanceof Cat){
Cat cat = (Cat)animal
cat.catching();
}
1.4.5 final 关键字
- final修饰的类不能被继承
- final修饰的方法不能重写
- final和static同时修饰的字段为常量,一般大写
private static final String ENTITY_NAME = "phoneSalePoints";
- final修饰成员变量、局部变量初始化后不能修改。
- final修饰的形参不能在方法中赋值
1.5 抽象类和接口
1.5.1 abstract 抽象类
- 抽象方法:使用abstract修饰的方法, 只有方法的声明部分,没有方法体
- 子类继承了抽象类, 子类需要重写抽象类所有的抽象方法
- ==面向抽象编程:==上层代码只定义规范(例如:abstract class Person),具体的业务逻辑由不同的子类实现,调用者并不关心
public class Main {
public static void main(String[] args) {
Person p = new Student();
p.run();
}
}
abstract class Person {
public abstract void run(); //抽象方法
}
class Student extends Person {
@Override
public void run() {
System.out.println("Student.run");
}
}
1.5.2 interface 接口
抽象类与接口比较
abstract class | interface | |
---|---|---|
继承 | 只能extends一个class | 可以implements多个interface |
属性 | 可以定义成员变量 | 不能定义成员变量 |
抽象方法 | 可以定义 | 可以定义 |
非抽象方法 | 不能定义 | 可以定义default方法 |
default方法:实现这个接口的类不必须重写的方法,需要再重写
- 接口和接口中的方法前面都省略了abstract
2. java核心类
2.1 字符串
- Java字符串String是不可变对象,字符串操作不改变原字符串内容,而是返回新字符串;
2.1.1 比较使用==和equals
- 在编译期会把所有相同的字符串当作一个对象放在常量池中
- == 是直接比较的两个对象的堆内存地址(比较的是两个引用类型的变量是否是同一个对象)
- equals(equalsIgnoreCase方法-忽略大小写比较)判断的是具体内容
//编译期"hello"和"HELLO"都在常量池中,"HELLO".toLowerCase()之后返回的新字符串"hello"放在堆中,地址不同
String s1 = "hello";
String s2 = "HELLO".toLowerCase();
System.out.println(s1 == s2); //false
System.out.println(s1.equals(s2)); //true
2.1.2 String类提供的常用方法
// 是否包含子串: contains()
"Hello".contains("ll"); // true
//搜索子串
"Hello".indexOf("l"); // 2
"Hello".lastIndexOf("l"); // 3
"Hello".startsWith("He"); // true
"Hello".endsWith("lo"); // true
//提取子串 substring()
"Hello".substring(2); // "llo"
"Hello".substring(2, 4); "ll"
//去除首尾空白 trim() 和 strip()
" \tHello\r\n ".trim(); // "Hello" 不可去除中文空格字符\u3000
"\u3000Hello\u3000".strip(); // "Hello" 可去除中文空格字符\u3000
" Hello ".stripLeading(); // "Hello "
" Hello ".stripTrailing(); // " Hello"
//是否空 isEmpty() 和 isBlank()
"".isEmpty(); // true,因为字符串长度为0
" ".isEmpty(); // false,因为字符串长度不为0 是否为空
" \n".isBlank(); // true,因为只包含空白字符 是否为空白字符串
" Hello ".isBlank(); // false,因为包含非空白字符
//替换子串 replace()
"Hello".replace('l', 'w'); // "hewwo",所有字符'l'被替换为'w'
"Hello".replace("ll", "~~"); // "he~~o",所有子串"ll"被替换为"~~"
//分割(结果是数组) split()
String s = "A,B,C,D";
String[] ss = s.split("\\,"); // {"A", "B", "C", "D"}
String s = "A^B@C#D";
String[] ss = s.split("\\^|@|#"); // {"A", "B", "C", "D"}
//拼接字符串 join()
String[] arr = {"A", "B", "C"};
String s = String.join("***", arr); // "A***B***C"
//类型转换
//别类型转字符串
String.valueOf(123); // "123"
String.valueOf(45.67); // "45.67"
String.valueOf(true); // "true"
String.valueOf(new Object()); // 类似java.lang.Object@636be97c
//字符串转int
int n1 = Integer.parseInt("123"); // 123
int n2 = Integer.parseInt("ff", 16); // 按十六进制转换,255
//字符串转Boolean
boolean b1 = Boolean.parseBoolean("true"); // true
boolean b2 = Boolean.parseBoolean("FALSE"); // false
2.1.3 StringBuilder
- 在拼接字符串时,每次循环都会创建新字符串对象,扔掉旧的字符串对象,很多都是临时对象,浪费内存。使用StringBuilder会分配缓冲区,不会创建临时对象,只输出最终的
StringBuilder sb = new StringBuilder(1024);
sb.append("Mr ")
.append("Bob")
.append("!")
.insert(0, "Hello, ");
System.out.println(sb.toString());
- StringBuilder是可变对象,用来拼接字符串。支持链式操作,每次返回本身this,然后接着往后追加。
- StringBuffer线程更安全,但效率低,极少用。
2.1.4 StringJoiner
- 用分隔符拼接数组成字符串,可添加首尾
String[] names = {"Bob", "Alice", "Grace"};
StringJoiner s = new StringJoiner(",","Hello ","!");
for (String name : names) {
s.add(name);
}
System.out.println(s.toString()); //Hello Bob,Alice,Grace!
- 若不用添加首尾,直接使用String.join()
String[] names = {"Bob", "Alice", "Grace"};
var s = String.join(", ", names); //Bob,Alice,Grace
2.1.5 包装类型
- int 基本类型数据转成引用类型数据Integer,Integer类就可以视为 int 的包装类。
- java核心库为每种基本类型都提供了对应的包装类型:
基本类型 | 对应的引用类型 |
---|---|
boolean | java.lang.Boolean |
byte | java.lang.Byte |
short | java.lang.Short |
int | java.lang.Integer |
long | java.lang.Long |
float | java.lang.Float |
double | java.lang.Double |
char | java.lang.Character |
- 装箱、拆箱 和 自动装箱、自动拆箱
- 都发生在编译阶段
int i = 100;
Integer n = Integer.valueOf(i); //装箱
int x = n.intValue(); //拆箱
------------------------------------------------------
Integer n = 100; // 自动装箱:编译器自动使用Integer.valueOf(int)
int x = n; // 自动拆箱:编译器自动使用Integer.intValue()
- 自动拆箱可能会报空指针异常NullPointerException
Integer n = null;
int i = n;
- 包装类型是引用数据类型,比较必须使用equals();
- 整数和浮点数的包装类型都继承自Number;
// 向上转型为Number:
Number num = new Integer(999);
// 获取byte, int, long, float, double:
byte b = num.byteValue();
int n = num.intValue();
long ln = num.longValue();
float f = num.floatValue();
double d = num.doubleValue();
2.1.6 JavaBean
- JavaBean是一种符合命名规范的class,它通过getter和setter来定义属性
- 规范:
若干private实例字段;
通过public方法来读写实例字段
public class Person {
private String name;
private int age;
public String getName() { return this.name; }
public void setName(String name) { this.name = name; }
public int getAge() { return this.age; }
public void setAge(int age) { this.age = age; }
}
2.1.7 枚举类
public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
if (day == Weekday.SAT || day == Weekday.SUN) {
System.out.println("Work at home!");
} else {
System.out.println("Work at office!");
}
}
}
enum Weekday {
SUN, MON, TUE, WED, THU, FRI, SAT;
}
- 编译之后
//编译之后
public final class Weekday extends Enum { // 继承自Enum,标记为final class
// 每个实例均为全局唯一:
public static final Weekday SUN = new Weekday ();
public static final Weekday MON = new Weekday ();
public static final Weekday TUE = new Weekday ();
// private构造方法,确保外部无法调用new操作符:
private Weekday () {}
}
- 使用 name() 返回常量名
String s = Weekday.SUN.name(); // "SUN"
2.1.8 Math类
Math类就是用来进行数学计算的,比如:绝对值、最大值、最小值、三角函数、对数等
- 其中生成一个0 <= x < 1的随机数
Math.random(); // 0.53907... 每次都不一样
3. 集合
- 存放对象的容器
- 集合分为单列集合(Collection)和双列集合(Map)
3.1 集合与数组对比
数组 | 集合 |
---|---|
长度固定 | 长度可变 |
存放基本类型数据和对象 | 存放对象,基本类型数据会自动装箱 |
{1, 2, 3} | [1, 2, 3] |
- 字符串与对象区别:
String x = “abcd”
String y = new String(“zbcd”);
X 指向的是方法区中的字符串常量对象。
Y 指向的是堆中的字符串对象,在堆中所产生的字符串对象,必须以方法区中的字符串常量对象为模板,将字符串常量对象的内容复制到堆中。
所以 new String(“abcd”) 有可能产生两个对象,一个在堆中,一个在方法区中。
3.2 集合类的继承关系
- list: 有序、可重复、有索引
- set: LinkedHashSet有序、不重复、无索引
- 以下图片均为转载:
3.3 Collection接口 和 Collections类
- Collection接口是单列集合最顶层的接口,定义了所有单列集合共性的方法,任意单列集合都可以使用
最前面是返回值类型
● boolean add(Object e) 把给定的对象添加到当前集合中
● void clear() 清空集合中所有的元素
● boolean remove(Object o) 把给定的对象在当前集合中删除
● boolean contains(Object o) 判断当前集合中是否包含给定的对象
● boolean isEmpty() 判断当前集合是否为空
● Iterator iterator() 迭代器,用来遍历集合中的元素的
● int size() 返回集合中元素的个数
● Object[] toArray() 把集合中的元素,存储到数组中
● Iterator : 迭代器
● Object next() 返回迭代的下一个元素
● boolean hasNext() 如果仍有元素可以迭代,则返回 true。
ArrayList有的:
● boolean addAll(Collection) 把一个集合元素都添加到当前集合中
● Object get() 获取指定位置的对象
● void set(index,Object) 替换
● int indexOf() 获取对象所处的位置
- Collections类是容器的工具类,就如同Arrays是数组的工具类。
方法名称 | 实例 |
---|---|
反转 | Collections.reverse(numbers); |
混淆 | Collections.shuffle(numbers); |
排序 | Collections.sort(numbers); |
交换 | Collections.swap(numbers,0,5); |
滚动 | Collections.rotate(numbers,2); 例:[1, 2, 3, 4, 5]–>[4, 5, 1, 2, 3 ] |
非线程安全转线程安全 | Collections.synchronizedList(numbers); |
3.4 泛型 Generic
- 在实际应用中,一个集合中一般只存储同一类型的数据,通过泛型约束集合中元素的类型
- 不指定泛型的容器,可以存放任何类型的元素
- 指定了泛型的容器,只能存放指定类型的元素以及其子类
List genericheros = new ArrayList();
List<Hero> genericheros = new ArrayList<Hero>();
- 使用泛型,可以在一个ArrayList中放两种不同的对象,其余对象都不能放(题目)。
创建一个父类,让两个类都继承,然后ArrayList指定泛型为这个父类
//让 Student类、Teacher类都继承 Person类
List<Person> people = new ArrayList<Person>();
people.add(new Student("zhangsan",18));
people.add(new Teacher("wanglaoshi"));
for (Person everyone : people) {
System.out.println(everyone );
}
3.5 ArrayList和List
- 源码
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
ArrayList实现了java.util.List接口
List heros = new ArrayList(); //多态,接口引用指向子类对象
3.6 三种循环
3.6.1 迭代器 Iterator
- 原理:Collection集合中元素的通用获取方式。取元素前先判断有没有,有则取;再判断,再取;直到把所有元素取出来
- 快捷键ctrl + alt + v
- 快捷键ctrl + shift+ f/r
- 使用iterator()方法获取迭代器,迭代器的游标指向第一个元素的前面
- iterator.hasNext()用于判断是否还有下个元素没访问
- iterator.next()返回下个元素, 游标下移
Collection<String> list = new ArrayList<>(); //创建集合
Iterator<String> it = list.iterator(); //使用iterator()方法获取迭代器对象.
while(it.hasNext()){ // hasNext()判断是否有下一个元素
String str = it.next(); // 取出集合中下一个元素.
}
- 使用迭代器 remove集合中的对象
//迭代器删除Integer
List<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
Iterator<Integer> it = list.iterator();
while (it.hasNext()) {
Integer a = it.next();
if (a > 3) {
it.remove();
}
}
for (Integer i : list) {
System.out.print(i);
}
// 结果:1 2 3
3.6.2 for 循环
- 注意:
- 数组有length属性
- String有 length( ) 方法
- 集合使用 size( ) 方法
//1.
int[] a={1,2,3};
System.out.println(a.length);
//2.
String s="hahah";
System.out.println(s.length());
//3.
for (int i = 0; i < heros.size(); i++) {
Hero h = heros.get(i);
System.out.println(h);
}
3.6.3 增强型 for 循环
- 不体现索引
List<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
for (Integer i : list) {
System.out.println(i);
}
3.7 LinkedList
3.7.1 概念 FIFO 和 FILO
FIFO:先进先出,在Java中又叫 Queue 队列
FILO:先进后出,在Java中又叫 Stack 栈
3.7.2 双向链表( 双端队列 ) Deque
- LinkedList除了实现List接口外,还实现了双向链表结构Deque,方便在头尾插入和删除数据
- 源码:
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
public interface Deque<E> extends Queue<E> {
- 链表结构: 与数组结构相比较,数组结构,就好像是电影院,每个位置都有标示,每个位置之间的间隔都是一样的。 而链表就相当于佛珠,每个珠子,只连接前一个和后一个,不用关心除此之外的其他佛珠在哪里。
- 举例:
注意:双向链表主要是使用Deque接口,所以创建对象的时候可以使用Deque接口引用指向子类LinkedList对象。不能使用List接口,否则需要强转。
//链表结构
LinkedList<Arms> ll = new LinkedList<Arms>();
//Deque可以
//Deque<Arms> ll = new LinkedList<Arms>();
//List需强转
//List<Arms> ll1 = new LinkedList<Arms>();
//((LinkedList<Arms>) ll).addLast(new Arms("duolanjian"));
ll.addLast(new Arms("duolanjian"));
ll.addLast(new Arms("duolandun"));
ll.addFirst(new Arms("duolanjie"));
for (Arms everyArms : ll) {
System.out.println(everyArms);
}
// name:duolanjie
// name:duolanjian
// name:duolandun
ll.removeFirst();
// name:duolanjian
// name:duolandun
ll.removeLast();
// name:duolanjian
3.7.3 队列 Queue
- 常用方法
offer 在最后添加元素
poll 取出第一个元素
peek 查看第一个元素
//队列
//offer 在最后添加元素
//poll 取出第一个元素
//peek 查看第一个元素
Queue<Arms> q = new LinkedList<Arms>();
q.offer(new Arms("pobai"));
q.offer(new Arms("lvcha"));
q.offer(new Arms("huopao"));
Arms p = q.peek(); //查看 name:pobai
q.poll(); //取 [name:lvcha, name:huopao]
System.out.println(p);
System.out.println(q);
3.7.4 队列 Queue 和 双端队列 Deque 对比
队列 | 双端队列 |
---|---|
Queue | Deque |
add | addFirst / addLast (下同) |
offer | offerFirst 添加 |
remove | removeFirst 删除首位(集合为空抛异常) |
poll | pollFirst 取出首位(集合为空为null) |
element | getFirst |
peek | peekFirst 查看 |
- 使用LinkedList实现Stack栈(先入后出)
/**
* ClassName:MyStack
* Description:使用LinkedList实现Stack栈
*/
public class MyStack {
private static LinkedList<Arms> list = new LinkedList<>();
//进栈方法
public void push(Arms a){
list.offerLast(a);
}
//出栈方法
public void pull(){
list.pollLast();
}
public static void main(String[] args) {
MyStack stack = new MyStack();
System.out.println("当前栈内元素:"+ list);
for (int i = 0; i < 5; i++) {
stack.push(new Arms("arms" + i));
System.out.println("入栈" + list);
}
for (int i = 0; i < 5; i++) {
stack.pull();
System.out.println("出栈" + list);
}
}
}
运行结果:
当前栈内元素:[]
入栈[name:arms0]
入栈[name:arms0, name:arms1]
入栈[name:arms0, name:arms1, name:arms2]
入栈[name:arms0, name:arms1, name:arms2, name:arms3]
入栈[name:arms0, name:arms1, name:arms2, name:arms3, name:arms4]
出栈[name:arms0, name:arms1, name:arms2, name:arms3]
出栈[name:arms0, name:arms1, name:arms2]
出栈[name:arms0, name:arms1]
出栈[name:arms0]
出栈[]
3.7.5 ArrayList 和 LinkedList 的对比
ArrayList | LinkedList | |
---|---|---|
定位(查) | 快 | 慢 |
插入删除数据(增删) | 慢 | 快 |
底层 | 数组 | 双向链表 |
- ArrayList是顺序结构,所以定位很快,指哪找哪。 就像电影院位置一样,有了电影票,一下就找到位置了。
- LinkedList 是链表结构,就像手里的一串佛珠,要找出第99个佛珠,必须得一个一个的数过去,所以定位慢
比较 ArrayList和LinkedList, 在最后面插入100000条数据,谁快谁慢?
答:一样快。因为是插入在最后面,所以对于数组而言,并没有一个移动很多数据的需要,所以也非常的快
在List的中间位置,插入10000条数据,比较ArrayList快,还是LinkedList快,并解释为什么?
答:在这个位置插入数据时
数组定位很快,插入数据比较慢
链表定位很慢,插入数据比较快
最后发现,当总数是10000条的时候,链表定位的总开支要比数组插入的总开支更多,所以最后整体表现,数组会更好。
3.8 二叉树
3.8.1 概念
二叉树由各种节点组成
- 特点:
每个节点都可以有左子节点,右子节点
每一个节点都有一个值
3.8.2 排序插入数据
- 示意图来自:how2j.cn
使用二叉树对 {67,7,30,73,10,0,78,81,10,74} 排序
- 原理:小、相同的放左边,大的放右边
1. 67 放在根节点
2. 7 比 67小,放在67的左节点
3. 30 比67 小,找到67的左节点7,30比7大,就放在7的右节点
4. 73 比67大, 放在67的右节点
- 使用二叉树排序,组装成List形式输出从小到大的一组数据
public class Node {
// 左子节点
public Node leftNode;
// 右子节点
public Node rightNode;
// 值
public Object value;
// 插入 数据
public void add(Object v) {
// 如果当前节点没有值,就把数据放在当前节点上
if (null == value) {
value = v;
}
// 如果当前节点有值,就进行判断,新增的值与当前值的大小关系
else {
// 新增的值,比当前值小或者相同
if ((Integer) v -((Integer)value) <= 0) {
if (null == leftNode) {
leftNode = new Node();
}
leftNode.add(v);
}
// 新增的值,比当前值大
else {
if (null == rightNode) {
rightNode = new Node();
}
rightNode.add(v);
}
}
}
// 中序遍历所有的节点组成常见的List形式
public List<Object> values() {
List<Object> values = new ArrayList<>();
// 右节点的遍历结果
if (null != rightNode) {
values.addAll(rightNode.values());
}
// 当前节点
values.add(value);
// 左节点的遍历结果
if (null != leftNode) {
values.addAll(leftNode.values());
}
return values;
}
public static void main(String[] args) {
int randoms[] = new int[] { 67, 7, 30, 73, 10, 0, 78, 81, 10, 74 };
Node roots = new Node();
for (int number : randoms) {
roots.add(number);
}
System.out.println(roots.values());
//[0, 7, 10, 10, 30, 67, 73, 74, 78, 81] 先遍历左节点为顺序
//[81, 78, 74, 73, 67, 30, 10, 10, 7, 0] 先遍历右节点为倒序
}
}
- 二叉树刚插完数据的结构
3.8.3 选择法、冒泡法、二叉树 三种排序性能区别
选择法: 第一个与之后每一个比较,比其小交换位置
冒泡法: 两个相邻的比较,左大右小时,交换位置
二叉树: 第一个数放中间,小、相同的放左边,大的放右边
- 结论:二叉树最快
初始化一个长度是40000的随机数字的数组
初始化完毕
接下来分别用3种算法进行排序
选择法排序,一共耗时 6865 毫秒
冒泡法排序,一共耗时 4388 毫秒
二叉树排序,一共耗时 84 毫秒
3.9 Set
3.9.1 HashSet
- HashSet 集合中元素的存储位置与对象的哈希码有关。
- 在含有Hash字样的集合中,元素的存储位置都与元素的hashCode()的返回值有关。
- HashSet 属于单列集合,无序不可重复。
HashSet自身并没有独立的实现,而是在里面封装了一个Map.
HashSet是作为Map的key而存在的
而value是一个命名为PRESENT的static的Object对象,因为是一个类属性,所以只会有一个。
private static final Object PRESENT = new Object();
static class Student {
String name;
int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age &&
Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public static void main(String[] args) {
HashSet<Student> hs = new HashSet<>();
Student s = new Student("zhangsan",12);
hs.add(s);
hs.add(new Student("lisi",11));
hs.add(new Student("wangwu",18));
hs.add(new Student("zhaoliu",21));
hs.add(new Student("sunqi",35));
hs.add(new Student("zhuba",28));
for (Student p1: hs) {
System.out.println(p1);
}
System.out.println(hs.contains(s)); //true
s.age = 99;
for (Student p1: hs) {
System.out.println(p1);
}
System.out.println(hs.contains(s)); //false
}
原因:
- hashSet集合中元素的存储位置与对象的哈希码有关系,哈希码是根据对象属性值算出来的。
- 对象s被add进hs中时,会根据s对象的hashCode()返回值计算出一个存储位置A,即:把s对象存到hashSet集合的A位置上
- 修改了s对象的age属性之后,hashCode()会根据属性值重新算出一个哈希码,再有哈希码确定一个新定存储位置B
- 第二次contains(s);是去hashSet中的B位置查找是否存在s对象,当前s对象在A位置上,因此为false
3.9.2 TreeSet
- TreeSet集合底层是TreeMap
向TreeSet集合添加元素,实际上是把该元素作为键添加到了底层TreeMap中
TreeSet集合实际上就是底层TreeMap的键的集合 - TreeSet集合实现了SortedSet接口, 可以对集合中元素进行自然排序, 要求集合中的元素必须是可比较的
- 创建TreeSet集合时,通过构造方法指定Comparator比较器
public static void main(String[] args) {
//1) 创建TreeSet集合,存储Person对象, 在构造方法中指定Comparator比较器,按姓名升序排序
TreeSet<Person> treeSet = new TreeSet<>(new Comparator<Person>() {
//在匿名内部类中重写接口的抽象方法
@Override
public int compare(Person o1, Person o2) {
//指定一个比较规则
return o1.name.compareTo(o2.name); //姓名升序
}
});
//2)添加元素
treeSet.add(new Person("lisi", 18));
treeSet.add(new Person("feifei", 28));
treeSet.add(new Person("yong", 35));
treeSet.add(new Person("bin", 36));
treeSet.add(new Person("zhang", 33));
//3)直接打印
System.out.println( treeSet ); //[Person{name='bin', age=36}, Person{name='feifei', age=28}, Person{name='lisi', age=18}, Person{name='yong', age=35}, Person{name='zhang', age=33}]
- 如果没有指定Comparator比较器, 要求元素的类必须实现Comparable接口
public class Person implements Comparable<Person>{
String name;
int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
Objects.equals(name, person.name);
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public int compareTo(Person o) {
return this.name.compareTo(o.name);
}
}
public static void main(String[] args) {
//1) 创建TreeSet集合,存储Person对象, 在构造方法中指定Comparator比较器,按姓名升序排序
TreeSet<Person> treeSet = new TreeSet<>(new Comparator<Person>() {
//在匿名内部类中重写接口的抽象方法
@Override
public int compare(Person o1, Person o2) {
//指定一个比较规则
return o1.name.compareTo(o2.name); //姓名升序
}
});
//2)添加元素
treeSet.add(new Person("lisi", 18));
treeSet.add(new Person("feifei", 28));
treeSet.add(new Person("yong", 35));
treeSet.add(new Person("bin", 36));
treeSet.add(new Person("zhang", 33));
//3)直接打印
System.out.println( treeSet ); //[Person{name='bin', age=36}, Person{name='feifei', age=28}, Person{name='lisi', age=18}, Person{name='yong', age=35}, Person{name='zhang', age=33}]
//treeSet3 是使用了第二种Person类实现Comparable接口
TreeSet<Person> treeSet3 = new TreeSet<>();
treeSet3.addAll(treeSet);
System.out.println( treeSet3);
}
- 原理:在TreeSet集合中, 是根据Comparator/Comparable的比较结果是否为0来判断是否为同一个对象。如果Comparator的compare()方法/Comparable的compareTo()方法的返回值为0 就认为是同一个对象。
public static void main(String[] args) {
//创建TreeSet集合,存储Person对象, 通过构造方法指定Comparator比较器,按年龄降序排序
TreeSet<Person> treeSet = new TreeSet<>(new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o2.age - o1.age;
}
});
//当前treeSet是根据年龄比较Person大小的, 在添加Person对象时, 如果年龄相同就认为是同一个对象
treeSet.add(new Person("lisi", 18));
treeSet.add(new Person("feifei", 18));
treeSet.add(new Person("zhang", 18));
treeSet.add(new Person("yong", 18));
System.out.println( treeSet.size() ); // 1
System.out.println( treeSet );
System.out.println( treeSet.contains( new Person("wang", 18))); //true
}
- true的原因是:compare()方法 或 compareTo()方法中进行的哪个属性的比较,当添加的对象的该属性值相同时,该方法返回值为0,就认为是相同的元素,set集合不允许存储相同的元素。
- 在set集合中比较对象是否重复,就看compare方法中那个属性值是否重复。
3.10 Collection集合小结
- Collection集合:
只能存储引用类型的数据, 单个存储
基本操作: add(), remove(), contains(), size(), iterator()
3.10.1 List集合
- 特点: 存储的元素是有序,可重复的
为每个元素指定一个索引值
增加的方法, 针对索引值的操作, listIterator(), subList(), sort(Comparator)
- ArrayList集合
底层是数组, 访问快, 添加/删除效率低
初始化容量: 10, 扩容: 1.5倍 - Vector集合
底层是数组, 它是线程安全的, ArrayList不是线程安全的
初始化容量: 10, 扩容: 2倍 - LinkedList集合
底层是双向链表, 添加/删除效率高, 访问慢 - List集合应用场景
ArrayList适用于以访问为主, 很少添加/删除的情况
LinkedList适用于经常添加/删除的情况
3.10.2 Set集合
- 特点: 数据无序,不可重复
- HashSet集合
底层是HashMap, HashSet实际上就是HashMap键的集合 - TreeSet集合
底层是TreeMap, TreeSet实际上就是TreeMap键的集合
TreeSet实现了SortedSet接口, 可以对元素自然排序, 要求元素必须是可比较的:
1)在构造方法中指定Comparator比较器对象
2)如果没有Comparator比较器, 集合元素的类必须实现Comparable接口 - Set集合的应用场景
如果不需要对Set集合进行排序就选择HashSet
如果需要对Set集合的元素进行排序就选择TreeSet
- 注意:
- List集合/HashSet集合的contains( e ) / remove( e )等方法需要调用对象的equals()方法, 这些集合中的元素的类需要重写equals()方法
- TreeSet集合中contains( e )/ remove( e) 等方法判断是否同一个对象是根据Comparator/Comparable的比较结果(对象某个属性)是否为0来判断的, 如果比较结果为0表示同一个元素
3.11 Map集合小结
- Map集合:
按<键,值>对的形式存储元素
基本操作: put( k, v), get( k ), containsKey( k ), containsValue( v ) , remove( k )
keyset() , values(), entrySet()
3.11.1 HashMap
- 底层是哈希表(散列表), 哈希表就是一个数组, 数组的每个元素是一个单向链表
线程不安全
初始化容量: 16, 扩容: 1.5倍, 加载因子: 0.75
当hashMap中<键,值>对的数量 > 哈希桶容量 * 加载因子时, 哈希桶(数组)要扩容 , 按2倍大小扩容
如果单向链表中结点的个数超过8个时, 系统会自动的把单向链表转换为树形结构 - 原理: key–>hash码–>hash函数–>hash值–>数组下标–>链表
- 如何解决哈希冲突?
链表法 - HashMap在jdk8中的改进:
红黑树:哈希值为节点的二叉排序树
3.11.2 HashTable
底层是哈希表, 线程安全
初始化容量: 11, 扩容: 2倍 + 1, 加载因子: 0.75
3.11.3 Properties
继承了HashTable, 键与值都是String类型
经常用于设置/读取系统属性值
一般情况下, 系统属性会保存在配置文件中, 可以通过Properties读取配置文件的内容, 也可以使用ResouceBundle读取配置文件的属性
3.11.4 TreeMap
实现了SortedMap接口, 可以根据键自然排序, 要求键必须是可比较的。TreeMap可以根据键自然排序, 排序的原理是二叉树中序遍历原理。
1. 指定Comparator比较器,
2. 键需要实现Comparable接口
3. Comparator比较与Comparable如何选择?
对于TreeMap来说, 先根据Comaparator比较器进行比较大小 , 如果没有Comparator比较器, 再选择Comparable接口
对于开发人员来说, 一般通过实现Comparable接口定义一个默认的比较规则 , 通过Comparator比较器定义若干不能同的排序规则
3.10.2 Set集合
- 特点: 数据无序,不可重复
- HashSet集合
底层是HashMap, HashSet实际上就是HashMap键的集合 - TreeSet集合
底层是TreeMap, TreeSet实际上就是TreeMap键的集合
TreeSet实现了SortedSet接口, 可以对元素自然排序, 要求元素必须是可比较的:
1)在构造方法中指定Comparator比较器对象
2)如果没有Comparator比较器, 集合元素的类必须实现Comparable接口 - Set集合的应用场景
如果不需要对Set集合进行排序就选择HashSet
如果需要对Set集合的元素进行排序就选择TreeSet
- 注意:
- List集合/HashSet集合的contains( e ) / remove( e )等方法需要调用对象的equals()方法, 这些集合中的元素的类需要重写equals()方法
- TreeSet集合中contains( e )/ remove( e) 等方法判断是否同一个对象是根据Comparator/Comparable的比较结果(对象某个属性)是否为0来判断的, 如果比较结果为0表示同一个元素
3.9 HashSet、TreeSet、HashMap、Hashtable、 和hashcode
3.9.2 HashMap
- HashMap属于双列集合,储存数据的方式是:键值对,键不能重复,值可重复。(若键重复,后面的值会覆盖前面的值)可以存放null,线程不安全。
3.9.3 Hashtable
- Hashtable同HashMap,但不能存放null,线程安全。
3.9.4 hashcode
- hashcode相当于字典的页码,先确定大概位置,再详细对比。
比如有一个对象,hashcode值为1001,保存时,存在一个数组1001的索引位置上,如果该位置没有其他对象,直接保存;如果已经有其他对象,就创建一个链表往后追加。
查询时,直接用hashcode定位到该索引位置,然后依次使用equals对比才位置对象。比起list全部循环一遍查找,大大提高了查询效率。
示意图来自:how2j
3.10 HashSet、LinkedHashSet 、TreeSet
- HashSet: 无序
- LinkedHashSet: 按照插入顺序
- TreeSet: 从小到大排序
- 都不重复