一些常见数据结构:
数组:增删慢、查找快,相邻元素内存地址相邻,当数据量大时动态扩容效率低而且非常耗费性能。
链表:增删快、查找慢,当数据量大时查找元素很慢,需要从头开始逐个遍历。链表可以分为单向链表、循环链表、双向链表、双向循环链表。
二叉树:类似于链表,只不过链表只有一个指向next,而二叉树有两个指向即left和right。如果二叉树存储有序元素(例如比根节点小存左子树,比根节点大存右子树),考虑二叉树平衡的情况,当查找一个元素时,每次比较都可以将查找范围缩小一半,类似于二分查找,查找效率非常高。
栈stack:先进后出,操作:压栈、弹栈
队列queue:先进先出,操作:入队、出队。也存在双向队列,即两边都可以出入队。
集合即容器,用来存储数据的结构。java内置了非常成熟的容器,集合在java.util包下
如何对这些容器中的元素遍历呢?可以使用Iterator迭代器,通过iterator()可以实现对集合中所有元素的遍历。
集合按照其存储结构可分为两大类:Collection是单列集合,Map是双列集合。
collection接口中定义的方法:
通常使用中不会直接使用Collection接口,而是使用其子接口
List
和Set
。其中list中允许元素重复,set不允许元素重复
list接口的实现类:ArrayList(95%)(数组实现)、Vector(4%)(数组实现)、LinkedList(1%)(双端链表实现),Vector可以看作ArrayList的早期版本。vector是线程安全的,arraylist是非线程安全的。如果不需要线程安全,推荐使用ArrayList代替Vector。(线程安全会降低性能)
数组实现,动态扩容每次1.5倍。默认大小是10,如果明确知道arraylist要存很多数据,就要给定初始值大小,否则会浪费很多内存在扩容上,会降低性能。
初始化大小10是在扩容操作中实现的。查看源码弄清楚扩容流程。
如果有参构造提供初始大小,则直接创建对应大小的数组,这一过程不需要扩容。
如果通过无参构造,默认赋值一个空数组,在首次添加元素时,进行扩容,数组默认长度为10,扩容是通过Arrays.copyOf实现的
数组实现。方法与ArrayList一致,多了一个构造方法,该方法多了一个参数即增量大小。Vector默认增量大小是0,如果增量大小>0每次扩容按照增量大小进行扩容,否则安装2倍大小扩容。
双向链表实现。不仅可以用作list,还可以当作栈、单端队列、双端队列来使用。
栈的操作:push()入栈、pop()出栈
队列操作:addFirst(),addLast()、removeFirst()、removeLast()、getFirst()、getLast()
迭代器只能迭代Collection集合,即list和set
iterater:hasNext()、next()等等
ListIterator:是Iterator的子类接口,除了上面的方法还有 hasPrevious()和previous()、add()、remove()等
遍历数组或集合,集合只能是Collection,即list和set
Set实现类:
HashSet
、TreeSet
、LinkedHashSet
基本上和Collection方法一致,仅添加了少许几个方法。
注意:Set中并没有get()方法,如果想要遍历一个set,可以通过iterator或者调用toArray()生成一个数组并对数组进行遍历。
hashset内部使用了hashmap(也叫做散列表),存储的数据是无序的,不能保证存储顺序。add方法在内部是调用了hashmap的put方法。
Set类如何保证值的不重复?答案是使用map类,因为map类存储的键值对,而键是不可重复的,所以利用这一点可以实现不重复。
内部是使用TreeMap实现的。TreeSet采用了二叉树存储,是有序的。
TreeSet和TreeMap若是存储自定义的类需实现Comparable接口,否则不能使用。
集合的迭代器一般是安全失败的,TreeSet的迭代器是快速失败的。
安全失败的意思:当一个迭代器对这个集合遍历时会首先把数据拷贝一份,对拷贝数据进行迭代,这样即使在遍历过程中其他线程对这个集合进行了修改也不会出错。而快速失败就是不加拷贝,直接对集合进行遍历,但若有其他线程对集合数据进行了增删修改,就会出错。
TreeSet是有序的,排序的类需要实现Comparable<>接口并重写compareTo方法,<>中是需要排序的类型。
public class TreeSetTest {
public static void main(String[] args) {
TreeSet set = new TreeSet<>();
Person p1 = new Person("张三", 20);
Person p2 = new Person("李四", 30);
Person p3 = new Person("王二", 10);
set.add(p1);
set.add(p2);
set.add(p3);
for (Person p :set)
System.out.println(p);
}
static class Person implements Comparable{
private String name;
private int age;
//重写此方法自定义排序规则,
//return值说明:负数this小,0相等,正数this大
@Override
public int compareTo(Person o) {
if (age o.age) return 1;
return 0;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + "\'" +
", age=" + age +
'}';
}
@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 int hashCode() {
return Objects.hash(name, 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 张三, String s) {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
}
Map的实现类:HashMap
、HashTable
、ConcurrentHashMap
、TreeMap
、LinkedHashMap
操作都是一样的。
存数据:put()
删除数据:remove(),会返回删除的这个数据,有些数据使用一次就不再使用了就可以使用remove()删除并获取
获取数据:get()
遍历数据:通过keySet()
遍历一个Map比较麻烦,可以使用keySet()获取所有键的set集合,通过遍历set中的键可以遍历所有值。
其他方法:containsKey()、containsValue()、size()、
values()将所有的值转换成collection集合,用的非常少。
hashMap原理图:
根据我的面试经验来看,hashmap是面试被问的最多的数据结构,没有之一,其次就是list。查看实现原理,而hashmap最重要的就是resize扩容方法!必须吃透源码!必须吃透源码!必须吃透源码!
作为HashMap键的那个类必须重写equals和hashCode方法
HashMap存储自定义对象时,如果自定义对象是键,则不要随意更改对象的数据,否则将查找不到原数据,例如:
public class Test{
public static void main(String[] args) {
HashMap<Book, String> map = new HashMap<>();
Book book1 = new Book("金苹果", "讲述了金苹果的故事。");
map.put(book1,"这是第一本书");
Book book2 = new Book("银苹果", "讲述了银苹果的故事。");
map.put(book2,"这是第二本书");
//将book1的name进行修改
book1.setName("铜苹果");
//get方法将根据book1的hashCode查找位置,找到位置后再进行equals比较
//打印结果为null
System.out.println(map.get(book1));
}
static class Book{
private String name;
private String info;
public Book(String name, String info) {
this.name = name;
this.info = info;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Book book = (Book) o;
return Objects.equals(name, book.name) &&
Objects.equals(info, book.info);
}
//计算hashCode是根据name和info的值计算的,如果这两个属性的值被修改,
//计算出的hashcode就会改变
@Override
public int hashCode() {
return Objects.hash(name, info);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
}
}
HashMap
、HashTable
、ConcurrentHashMap
区别TreeMap不保证存储顺序但会对元素进行排序(根据键排序),LinkedHashMap可以保证存储顺序。实现原理是同时使用HashMap和双向链表保存数据,双向链表保证了存储顺序。
List、Set、Map 这三个接口中提供了一些静态方法,of(),该方法可以创建固定长度的集合,不可向集合中添加、删除元素。
public static void main(String[] args) {
List<Book> list = List.of(new Book("1","100"),new Book("2","200"));
//r
//list.remove(0);
for (Book s :
list) {
System.out.println(s);
}
}
在这个实现中,使用了泛型,实现了添加数据、指定位置添加数据、删除指定位置数据、获取指定位置数据、获取size的一些操作。
public class SingleList<a> {
private Node<a> head;
private int size;
//添加一个数据,默认是添加到结尾
public boolean add(a data){
Node<a> cur = new Node<>(data);
if (head==null)
head = cur;
else{
Node tmp = head;
while (tmp.getNext()!=null){
tmp = tmp.getNext();
}
tmp.setNext(cur);
}
size++;
return true;
}
//指定位置插入数据
public boolean add(a data,int index){
if (index>=size){
throw new RuntimeException("下标越界异常!index:"+index+",size:"+size);
}
else{
Node<a> cur = new Node<>(data);
if (index == 0){
cur.setNext(head);
head = cur;
}
else{
Node tmp = head;
index-=1;
while (index-->0){
tmp=tmp.getNext();
}
cur.setNext(tmp.getNext());
tmp.setNext(cur);
}
size++;
return true;
}
}
//指定位置删除
public a delete(int index){
if (index>=size)throw new RuntimeException("下标越界异常!index:"+index+",size:"+size);
else{
a data;
if (index == 0){
data = head.getData();
head = head.getNext();
}else{
Node<a> right = head;
Node left = null;
while (index-->0){
left = right;
right = right.getNext();
}
data = right.getData();
left.setNext(right.getNext());
right.setNext(null);
}
size--;
return data;
}
}
//获取指定位置的数据
public a get(int index){
if (index>=size)throw new RuntimeException("下标越界异常!index:"+index+",size:"+size);
else {
Node<a> tmp = head;
while (index-->0){
tmp = tmp.getNext();
}
return tmp.getData();
}
}
//打印当前链表
public void print(){
Node tmp = head;
while (tmp!=null){
System.out.println(tmp.getData());
tmp = tmp.getNext();
}
}
//返回节点个数
public int size(){
return this.size;
}
}
//节点的定义
class Node<T>{
private T data;
private Node<T> next;
public Node() {
}
public Node(T data) {
this.data = data;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public Node<T> getNext() {
return next;
}
public void setNext(Node<T> next) {
this.next = next;
}
}
遍历二叉树通常有两种方式:广度优先搜索(BFS)、深度优先搜索(DFS)
说到底,搜索就是遍历,XX搜索不就是遍历某种数据结构的方法吗?搞这么专业的名词就是用来唬人的,广度优先搜索就是按层去遍历,深度优先搜索就是按深度去遍历。其中深度优先搜索又分为三种方式:
先序遍历、中序遍历、后序遍历。
首先来看广度优先搜索
BFS和DFS
LinkedList
可以当作list使用,也可以当作queue使用,还可以当作stack使用。BFS一般需要借助queue来实现。
DFS一般是递归实现,也可以通过循环实现,但非常麻烦。
前序:根左右。中序:左根右。后序:左右根。
三种方式无非是打印根节点的时间不一样,前序先打印根,中序中间打印根,后续最后打印根。
import java.util.LinkedList;
public class Tree {
public static void main(String[] args) {
//创建7个节点
TreeNode<Integer> root = new TreeNode<>(1);
TreeNode<Integer> node2 = new TreeNode<>(2);
TreeNode<Integer> node3 = new TreeNode<>(3);
TreeNode<Integer> node4 = new TreeNode<>(4);
TreeNode<Integer> node5 = new TreeNode<>(5);
TreeNode<Integer> node6 = new TreeNode<>(6);
TreeNode<Integer> node7 = new TreeNode<>(7);
//手动构建一棵树
root.setLeft(node2);
root.setRight(node3);
node2.setLeft(node4);
node2.setRight(node5);
node3.setLeft(node6);
node3.setRight(node7);
//测试广度优先搜索
BFS(root);
//测试深度优先搜索
DFS(root);
}
static void BFS(TreeNode root){
LinkedList<TreeNode> queue = new LinkedList<>();
//首先将根节点放入队列
queue.addLast(root);
int i = 0;
while (queue.size()>0){
i++;
TreeNode cur;
//size为该层节点的个数
int size = queue.size();
System.out.print("第"+i+"层数据:");
while (size-->0){
//获取第一个节点为当前节点cur
cur = queue.removeFirst();
//将此节点的左右节点放入队列
if (cur.getLeft()!=null)
queue.addLast(cur.getLeft());
if (cur.getRight()!=null)
queue.addLast(cur.getRight());
//输出这一层的数据,不换行
System.out.print(cur.getData()+"\t");
}
//每输出完一层需要换行
System.out.println();
}
}
//DFS
static void DFS(TreeNode root){
if (root == null)return;
if (root.getLeft()!=null)
DFS(root.getLeft());
if (root.getRight()!=null)
DFS(root.getRight());
System.out.print(root.getData()+"\t");
}
}
//定义树节点
class TreeNode<T>{
private T data;
private TreeNode left;
private TreeNode right;
public TreeNode() {
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public TreeNode getLeft() {
return left;
}
public void setLeft(TreeNode left) {
this.left = left;
}
public TreeNode getRight() {
return right;
}
public void setRight(TreeNode right) {
this.right = right;
}
public TreeNode(T data) {
this.data = data;
}
}
上面通过手动构建了一棵二叉树,二叉树相较于链表最大的优势就在于二叉树可以对数据进行排序,一棵平衡的有序二叉树查找元素的效率非常高,性能接近于二分查找,所以如何构建有序二叉树呢?
有序二叉树的中序输出即为排好序的数据
public class BinarySortTree {
private TreeNode<Integer> root;
//添加数据
public boolean add(Integer data){
if (root == null){
root = new TreeNode<>(data);
return true;
}
else{
TreeNode<Integer> cur = root;
TreeNode<Integer> parent ;
while(cur!=null){
parent = cur;
if (data<cur.getData()){
cur = cur.getLeft();
if (cur == null){
parent.setLeft( new TreeNode<>(data));
return true;
}
}else{
cur= cur.getRight();
if (cur == null){
parent.setRight( new TreeNode<>(data));
return true;
}
}
}
}
return false;
}
//查找目标值
//这种查找方式是借助了二叉排序树的特点,只有是二叉排序树才能这样查找数据
public TreeNode<Integer> get(Integer target){
TreeNode<Integer> cur = root;
while (cur!=null){
if (cur.getData() == target)
return cur;
else if (target<cur.getData())
cur = cur.getLeft();
else cur = cur.getRight();
}
return null;
}
//获取根节点
public TreeNode getRoot(){
return root;
}
}
//测试
public static void main(String[] args) {
BinarySortTree binarySortTree = new BinarySortTree();
int[] num = {2,4,5,1,3,4,0};
for (int i = 0; i < 7; i++) {
binarySortTree.add(num[i]);
}
TreeNode root = binarySortTree.getRoot();
//DFS的中序输出即为 0 1 2 3 4 4 5
Tree.DFS(root);
}
talk is cheap show me code
//三种创建文件的方式
public static void main(String[] args) throws IOException {
File dir = new File("z://haha");
//创建haha文件夹
//System.out.println(dir.mkdir());
File a = new File(dir, "a.txt");
System.out.println(a.createNewFile());
File b = new File("z:haha", "b.txt");
System.out.println(b.createNewFile());
//删除文件
a.delete();
b.delete();
}
其他一些常用方法:
getAbsolutePath()获取绝对路径,例如:z://haha
length()获取文件的长度,即大小,单位是字节
getPath()、getParent()、getParentFile()
exist()判断一个文件是否存在
list()、listFile()、
renameTo()重命名绝对路径,可以看作移动位置并重命名
以下是一个遍历文件夹的例子:遍历z盘并找到所有MP4文件
public class SearchAllDir {
public static void main(String[] args) {
File z = new File("z:");
File[] list = z.listFiles();
search(list);
}
//传入File数组
static void search(File[] list){
//如果不为null且长度大于0
if (list!=null&&list.length>0){
//遍历文件
for (File file : list) {
//如果是文件
if (file.isFile()){
//匹配以.mp4结尾的文件并输出其绝对路径,还可以加上对大小的判断
if (file.getName().endsWith(".mp4")){
System.out.println("找到一个mp4:"+file.getAbsolutePath());
}
}
//否则是文件夹,递归搜索
else{
search(file.listFiles());
}
}
}
}
}
在java中相对路径根目录是项目目录。
例如:
File a = new File("a.txt");
System.out.println(a.getAbsolutePath());
//在Test项目中,打印结果如下:
IO流分类:
1.按照流的方向:输入流和输出流
2.按照流动的数据类型分类:字节流和字符流
注意:字符流也来自字节流,只不过字符流对字节流进行了一些处理,
一般使用字节流较多,但通过字节流读取文字可能会乱码,所以读取文字一般使用字符流。
任何流在传输时都是二进制
顶级父类如下:
任何流,在使用完之后都应该尽可能早的调用close()关闭流。
有一个例子:有时候我们要关闭某个文件却关不掉,原因就是有其他某个进程在读取这个文件没有关闭流。
常用方法:close()、flush()刷新输出流并强制写出缓冲区、write()写入输出流
输出流用的最多的就是FileOutputStream
常用方法:构造方法、close方法、write方法
创建一个流输出时默认是清空文件后再输入,也可以添加参数true,就变成追加模式。在一个流close()前的多次输出都是追加的。
//这样创建流默认是清空文件再输出
FileOutputStream fos = new FileOutputStream("z:\\IO\\io.txt");
//这样创建流是追加输出
FileOutputStream fos = new FileOutputStream("z:\\IO\\io.txt",true);
//传入int类型 int类型只有0-255有效,即低8位有效,后面24位都没用 65输出就是'A'
fos.write(65);
//传入一个byte[],这样用的并不多,因为文字一般使用字符输出流
fos.write("你好今天天气真好!&*)!".getBytes());
//传入byte[]数组,并指定开始下标和长度。
byte[] bytes = "ABCDE".getBytes();
fos.write(bytes,2,3);
三种输出方式:
InputStream中最常用的子类就是FileInputStream类。
常用方法还是三个:构造方法、close方法、read方法。read方法可以单字节读取,但更常用的是每次读取一个byte数组,这样可以减少IO次数。
例如,读取一个1MB的文件,如果单字节读取需要读取1024*1024次,但如果定义一个大小为1024*1024的byte数组,读取一次就可以了。
//read每次读取一个字节,得到的是int类型,如果没有内容则返回-1
public static void main(String[] args) throws IOException {
//io.txt中内容是ABCDE
FileInputStream fis = new FileInputStream("z:\\IO\\io.txt");
int flag;
while (true){
flag = fis.read();
if (flag==-1)break;
System.out.println((char) flag);
}
fis.close();
}
//read(byte[] bytes)每次将读取的结果放入bytes数组中,io.txt中是A-Z
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("z:\\IO\\io.txt");
byte[] bytes = new byte[10];
fis.read(bytes);
//通过new String(byte[] bytes)可以构造字符串
System.out.println(new String(bytes));
fis.read(bytes);
System.out.println(new String(bytes));
fis.read(bytes);
System.out.println(new String(bytes));
fis.close();
}
打印结果如下:
可以发现,最后有四位是重复的,而qrst是上次读取的内容,读取到z后没有内容了,就没办法覆盖后四位了。
如何解决?
//io.txt中是A-Z
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("z:\\IO\\io.txt");
byte[] bytes = new byte[10];
int len = 0;
while (len!=-1){
//read(byte[] bytes)返回读取的字节长度,前两次都读取了10个字节,长度为10,
//第三次读取了6个字节,返回6,第四次由于没有内容了,返回-1
len = fis.read(bytes);
if (len == -1)break;
//0代表起始位置,len代表长度
System.out.println(new String(bytes,0,len));
}
fis.close();
}
打印结果如下:
最初只有ASCII码,只能表示英文字母、阿拉伯数字和一些常见符号。随着发展各国语言都有自己对应的编码表,为了解决各国编码不统一的问题,出现了utf-8编码,utf-8是一种可变长的编码表。长度可以是1-4个字节。
由于utf-8是可变长的,所以通过字节流读取中文可能会出现读一半汉字的情况,如图:
如果是乱码还比较容易解决,但这种读一半文字的情况就难以解决。为了解决这个问题,所以在读取文字时都是使用字节流。
public static void main(String[] args) throws IOException {
FileWriter fw = new FileWriter("z:\\IO\\io.txt");
//append方法返回调用者本身,所以可以连续调用,即链式调用
fw.append("你好!").append("张三风",0,2);
fw.close();
}
//演示了每次读取一组字符
public static void main(String[] args) throws IOException {
FileReader fr = new FileReader("z:\\IO\\io.txt");
char[] res = new char[2];
while (true){
int len = fr.read(res);
if (len == -1)break;
//读取多少长度,就输出多少长度,记得从0-len
System.out.println(new String(res,0,len));
}
}
flush,将缓冲区的字符强制输出。close()方法中也调用了flush方法。字符流在读取字符后一定要刷新管道!
假设字节流是获取到的,代码中手动创建了。
将配置输出到指定位置或者从指定位置读取配置。
public static void main(String[] args) throws IOException {
/* Properties ppt = new Properties();
ppt.setProperty("1","☞1");
ppt.setProperty("key2","value2");
FileWriter fr = new FileWriter("z:\\IO\\test.properties");
ppt.store(fr,"这是属性配置!");
fr.close();*/
Properties ppt = new Properties();
FileReader fr = new FileReader("z:\\IO\\test.properties");
ppt.load(fr);
System.out.println(ppt.getProperty("1"));
System.out.println(ppt.getProperty("key2"));
}
序列化:将对象以文件的方式存储起来。
由于这种技术异常的简单方便,导致大量开发人员使用这种技术。随着时间的推移,java官方发现大量bug是由于序列化技术导致的,因此18年java官方建议停止使用序列化技术。
public class Xuliehua {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//序列化
/* ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("z:\\IO\\xuliehua"));
Person p = new Person("张三", 20);
oos.writeObject(p);
oos.close();*/
//反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("z:\\IO\\xuliehua"));
Object p = ois.readObject();
System.out.println(p);
}
//被序列化的对象必须实现Serializable接口,该接口是一个标记接口,无任何方法。
static class Person implements Serializable {
private String name;
private int age;
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + 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;
}
}
}
如果一个类中引用了其他类,那么这个其他类也需要实现序列化接口才能实现序列化。
部分属性序列化的几种方式:
- transient。只需在对应的属性前加上这给关键字即可
- static
- 默认方法writeObject和readObject
- Externalizable接口
继承Thread类,并重写run方法。
每个线程都有自己的栈空间,多个线程共享一个堆空间。
实现Runnable接口,并重写run方法。
实现Runnable接口与继承Thread类的优势:
1、是通过创建任务,然后给线程分配的方式实现的多线程。更适合多个线程执行相同任务的情况。
2、可以避免单继承带来的局限性
3、任务与线程本身是分离的,提高程序的健壮性
4、线程池可以接收Runnable类型,而不能接收Thread类
因此平时使用Runnable实现多线程更多。
//方法一:自己写一个类实现runnable接口并重写run方法
public static void main(String[] args) {
//创建一个任务对象(常规创建任务对象)
MyRunnable myRunnable = new MyRunnable();
//将任务对象交给一个线程
Thread t = new Thread(myRunnable);
//线程启动
t.start();
for (int i = 0; i < 10; i++) {
System.out.println("木兰当户织"+i);
}
}
----------------------------------------------------------
//直接new 一个任务对象并当作参数传给线程
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("唧唧复唧唧"+i);
}
}
});
t.start();
for (int i = 0; i < 10; i++) {
System.out.println("木兰当户织"+i);
}
}
------------------------------------------------------
//上面那种方法的lambda方式
public static void main(String[] args) {
Thread t = new Thread(()-> {
for (int i = 0; i < 10; i++) {
System.out.println("唧唧复唧唧"+i);
}
});
t.start();
for (int i = 0; i < 10; i++) {
System.out.println("木兰当户织"+i);
}
}
---------------------------------------------------------
//通过thread的匿名内部类也可以简单方便的创建一个线程
new Thread(){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("唧唧复唧唧"+i);
}
}
}.start();
---------------------------------------------------------
//上面的lambda写法
new Thread(()->{
for (int i = 0; i < 10; i++) {
System.out.println("唧唧复唧唧"+i);
}
}).start();
最常用的一些方法,仅列出部分:
sleep()、start()、setPriority(int newPriority)
设置优先级、setDaemon(boolean on)
设置守护线程
一个线程的生命应该由线程本身决定,不能强行关闭一个线程,否则可能导致问题。如:一个线程执行一半突然被强行关闭,但线程还占用着某些资源没有释放,就会导致这些资源无法得到释放。
java早期提供一个stop方法来结束一个线程,但现在已经弃用了。
通过interrupt()
方法,相当于一个标记,在调用完这个方法后,会进入catch块处理,由人手动处理决定该线程是否要死亡,释放资源等操作也在catch中执行。
public static void main(String[] args) throws InterruptedException {
//lambda写法
Thread t = new Thread(()->{
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("发现中断,进入catch开始处理");
//进行关闭资源等操作然后return就关闭线程了
return;
}
}
});
t.start();
for (int i = 0; i <5; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
Thread.sleep(1000);
}
//五秒后t线程中断,之后进入catch处理
t.interrupt();
}
用户线程:例如main线程和创建的其他线程都是用户线程,当所有用户线程结束时程序就结束了。
守护线程:守护用户线程的线程,不能决定自己的生命周期,当最后一个用户线程结束时所有守护线程都会自动结束。
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//将t线程设置为守护线程
t.setDaemon(true);
t.start();
//当main线程结束后 t线程没有执行完但仍然会结束
for (int i = 0; i <5; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
Thread.sleep(1000);
}
}
public static void main(String[] args) {
//创建一个任务对象,将这个任务分配给多个线程执行,可能会出现余票为负数的问题
Runnable t = new Ticket();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
static class Ticket implements Runnable{
private int count = 10;
@Override
public void run() {
while (count> 0){
System.out.println(Thread.currentThread().getName()+"准备开始卖票");
count--;
System.out.println(Thread.currentThread().getName()+"卖票结束,余票:"+count);
}
}
}
三种同步的方式:
前两种属于隐式锁,第三种属于显示锁
- 同步代码块:在一个代码块前使用synchronized关键字,格式为:synchronized () { 代码块 }
- 同步方法:在方法上使用synchronized关键字,锁的对象是this
- 显式锁:Lock l = new ReentrantLock();创建锁对象,l.lock();进行上锁l.unlock();进行解锁
公平锁和非公平锁:公平锁是先来先使用,当锁解开后先到的线程先使用;非公平锁是所有等待的线程进行抢占,抢到锁的线程先使用。
Java默认是非公平锁。如何实现公平锁?在第三种方法中,创建显示锁的时候给一个参数true,就是公平锁。
生产者-消费者问题
创建FutureTask对象 , 并传入第一步编写的Callable类对象 FutureTask future = new FutureTask<>(callable);
通过Thread,启动线程 new Thread(future).start()
频繁创建和销毁线程非常耗时,线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建和销毁线程的操作,节约了大量时间和资源。
线程池主要:包含一个线程数组和一个任务列表。
Java中四种线程池:(简略流程)
Executor, ExecutorService 和 Executors区别与联系:
Executor, ExecutorService 都是接口,ExecutorService继承于Executor,Executors是工具类,他提供对ThreadPoolExecutor的封装产生ExecutorService的具体实现类。
public class ThreadPool_1 {
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newCachedThreadPool();
//指挥线程池执行新的任务,前两个通过匿名内部类
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"床前明月光");
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"李白喝豆浆");
}
});
//后面两个通过lambda
service.execute(()->{
System.out.println(Thread.currentThread().getName()+"喝了一大缸");
});
//主线程睡眠0.1秒后,前三个进程都执行完毕,变成空闲状态,第四个任务就不会创建新的线程
Thread.sleep(100);
service.execute(()->{
System.out.println(Thread.currentThread().getName()+"拉了一裤裆");
});
}
}
执行结果:
public class ThreadPool_2 {
public static void main(String[] args) throws InterruptedException {
//定长线程池 长度为2,只有两个线程
ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(()->{
System.out.println(Thread.currentThread().getName()+"窗前明月光");
});
service.execute(()->{
System.out.println(Thread.currentThread().getName()+"窗前明月光");
});
service.execute(()->{
System.out.println(Thread.currentThread().getName()+"窗前明月光");
});
service.execute(()->{
System.out.println(Thread.currentThread().getName()+"窗前明月光");
});
}
}
执行结果:
public class ThreadPool_3 {
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(()->{
System.out.println(Thread.currentThread().getName()+"放假啊圣诞快乐发");
});
service.execute(()->{
System.out.println(Thread.currentThread().getName()+"放假啊圣诞快乐发");
});
service.execute(()->{
System.out.println(Thread.currentThread().getName()+"放假啊圣诞快乐发");
});
//shutdown用于关闭线程池
service.shutdown();
}
}
public class ThreadPool_4 {
public static void main(String[] args) throws InterruptedException {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
scheduledExecutorService.schedule(()->{
System.out.println(Thread.currentThread().getName()+"fsajdl");
},5, TimeUnit.SECONDS);
scheduledExecutorService.scheduleAtFixedRate(()->{
System.out.println(Thread.currentThread().getName()+"fsajdl");
},3,2,TimeUnit.SECONDS);
scheduledExecutorService.shutdown();
}
}
使用lambda表达式注意:接口中必须只有一个方法才能使用lambda表达式。
相关知识仅做粗略的解释说明,没有详细深入。
ip:所有连接到计算机网络中都有一个ip地址,ip地址就像你家的地址,每个ip地址(公网ip)在全球网络中都是唯一的。
ipv4:长度为32位。分为a,b,c,d,e五类,总共有42亿个左右。由于ipv4已经用尽,所以出现了ipv6,ipv6数量之多是用不完的。
域名:相当于ip的别名,如果直接记忆ip非常不好记,通过别名可以更好的记住一个网站。
例如:域名之于ip就像通信录中人名之于手机号。
ip------------------域名
手机号-----------人名
端口号:0-65535 其中0-1024是系统或一些知名软件占用了,所以应当避免使用0-1024的端口号。
通过ip可以找到你的设备,但你的设备上运行了很多程序,那么别人发过来的数据怎么知道将数据发给哪个程序呢?例如:快递送进一个小区,如何将快递准确的送给每个人呢?是通过楼号。计算机中端口号就是这个楼号。每个程序可以占用n个端口号(通常占用1-2个就够了),通过端口号就可以知道数据分别是哪个应用程序的,从而准确的将所有数据送达相应的程序上。因此就避免了这种情况:别人给你发微信,数据传到你电脑后把数据给qq了。
协议:tcp/ip协议簇指代一系列的网络协议
网络编程程序分类:
- B/S程序:浏览器与服务器程序。即通过浏览器访问的服务,例如百度搜索。
- C/S程序:客户端与服务器程序。通过客户端访问的服务。例如qq。
B/S程序用户每次访问都是最新的服务,无需用户更新。但是B/S程序运行在浏览器中,而浏览器是别人写的,不能像C/S程序那样想怎么发数据就怎么发数据,必须按照特定的方式编写程序。因此B/S程序安全性不如C/S程序。
C/S程序用户需要手动下载新的客户端才能使用新版本,C/S程序如果被破解将难以解决,因为即使你更新了版本修复了Bug,但是用户不下载新版本也没办法。
编写C/S程序需要用到两个类:
- ServerSocket 搭建服务器
- Socket 搭建客户端,连接服务器。
双方使用socket(套接字)进行通信。编写C/S程序首先编写服务器,其次再编写客户端。
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerDemo {
/**
* TCP协议的网络编程
* 服务端,使用多线程处理
* */
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("服务器启动成功!");
while (true){
Socket socket = serverSocket.accept();
System.out.println("一个客户端连接了");
new Thread(()->{
try {
System.out.println("当前服务线程:"+Thread.currentThread().getName());
//如果服务端先输出,客户端就要先接收;如果客户端先接收,服务端就要先输出
//输出与接收必须成对出现
OutputStream os = socket.getOutputStream();
//将输出流转换成
PrintStream ps = new PrintStream(os);
ps.println(Thread.currentThread().getName()+"为您服务!");
InputStream is = socket.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is));
String s = bufferedReader.readLine();
System.out.println(s);
System.out.println("服务结束,已关闭服务!");
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
}
}
import java.io.*;
import java.net.Socket;
public class ClientDemo {
/**
* 客户端
* */
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1",6666);
InputStream is = socket.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is));
String s = bufferedReader.readLine();
System.out.println("接受到服务器发来的消息:"+s);
OutputStream os = socket.getOutputStream();
PrintStream ps = new PrintStream(os);
ps.println("你好服务器!这是一条来自客户端的消息!");
}
}
java可以将对象序列化,为什么还需要XML或JSON呢?
java序列化之后只有java语言可以解析序列化文件,其他语言是无法解析的。
例如:客户端使用c编写,而服务端使用java编写,客户端是识别不了java的序列化数据的。
XML可以用于:
网络数据传输 ❌(现在基本上都是使用JSON)
JSON解析性能比XML更高,而且数据量越大差距越明显,但XML可读性更强。XML重点用于编写配置文件。
数据存储❌(基本不用)
配置文件(XML主要用于配置文件)
XML重点在于 掌握语法格式,解析XML了解即可。
解析XML的方式:
此外还有Jdom和dom4j都是基于dom方式。
其中jdom包含了很多java类方便java开发,性能不是很高,而且不够灵活(因为是面向类编写的)
dom4j用有更高的性能和更高的灵活性(因为是面向接口编写的),此外还可以通过XPATH的方式解析。
Dom4j是一个非常优秀的开源的java xml api,性能优异,功能强大,易于使用。
许多开源项目都使用Dom4j来读写xml,如Hibernate
JSON是一种轻量级数据交换格式。
JSON(JavaScript Object Notation,JS对象简谱)是一种独立于语言的数据存储格式,是欧洲提出来的JavaScript中的一种规范。
在解析上:解析json速度比解析xml快,
在存储数据上:json表示的数据占用空间更小,更利于传输。
json出现于1999,在2005左右逐渐取代xml的地位,得益于以上一些优点。
对象格式对比:
Json使用频率非常之高,后端开发经常使用Json。
Java没有内置的解析Json的类,解析Json最常用的是:
- Gson:谷歌的
- FastJson:阿里的
//Gson和fastJson的使用,如果json中的属性是数组类型,会被解析成List类型
public class JsonTest {
public static void main(String[] args) {
Person p = new Person("张三",20,"北京市大兴区");
/* Gson g = new Gson();
//将对象转成json
String s = g.toJson(p);
System.out.println("将对象转换成json是:");
System.out.println(s);
//将json转成对象
Person p2 = g.fromJson(s, Person.class);
System.out.println("将json转成对象是:");
System.out.println(p2);*/
//阿里的fastjson
String s = JSON.toJSONString(p);
System.out.println(s);
Person p2 = JSON.parseObject(s,Person.class);
System.out.println(p2);
}
}