java学习笔记-2

1-集合

1.链表、二叉树

一些常见数据结构:

数组:增删慢、查找快,相邻元素内存地址相邻,当数据量大时动态扩容效率低而且非常耗费性能。

链表:增删快、查找慢,当数据量大时查找元素很慢,需要从头开始逐个遍历。链表可以分为单向链表、循环链表、双向链表、双向循环链表。

二叉树:类似于链表,只不过链表只有一个指向next,而二叉树有两个指向即left和right。如果二叉树存储有序元素(例如比根节点小存左子树,比根节点大存右子树),考虑二叉树平衡的情况,当查找一个元素时,每次比较都可以将查找范围缩小一半,类似于二分查找,查找效率非常高。

栈stack:先进后出,操作:压栈、弹栈

队列queue:先进先出,操作:入队、出队。也存在双向队列,即两边都可以出入队。

2.集合

集合即容器,用来存储数据的结构。java内置了非常成熟的容器,集合在java.util包下

在这里插入图片描述

如何对这些容器中的元素遍历呢?可以使用Iterator迭代器,通过iterator()可以实现对集合中所有元素的遍历。

集合按照其存储结构可分为两大类:Collection是单列集合,Map是双列集合。

collection接口中定义的方法:

在这里插入图片描述

通常使用中不会直接使用Collection接口,而是使用其子接口ListSet其中list中允许元素重复,set不允许元素重复

3.List接口(重点)

list接口的实现类:ArrayList(95%)(数组实现)、Vector(4%)(数组实现)、LinkedList(1%)(双端链表实现),Vector可以看作ArrayList的早期版本。vector是线程安全的,arraylist是非线程安全的。如果不需要线程安全,推荐使用ArrayList代替Vector。(线程安全会降低性能)

4.ArrayList类(重点)

数组实现,动态扩容每次1.5倍。默认大小是10,如果明确知道arraylist要存很多数据,就要给定初始值大小,否则会浪费很多内存在扩容上,会降低性能。

初始化大小10是在扩容操作中实现的。查看源码弄清楚扩容流程。

如果有参构造提供初始大小,则直接创建对应大小的数组,这一过程不需要扩容。

如果通过无参构造,默认赋值一个空数组,在首次添加元素时,进行扩容,数组默认长度为10,扩容是通过Arrays.copyOf实现的

5.Vector类

数组实现。方法与ArrayList一致,多了一个构造方法,该方法多了一个参数即增量大小。Vector默认增量大小是0,如果增量大小>0每次扩容按照增量大小进行扩容,否则安装2倍大小扩容。

6.LinkedList类

双向链表实现。不仅可以用作list,还可以当作栈、单端队列、双端队列来使用。

栈的操作:push()入栈、pop()出栈

队列操作:addFirst(),addLast()、removeFirst()、removeLast()、getFirst()、getLast()

7.Iterator和ListIterator迭代器

迭代器只能迭代Collection集合,即list和set

iterater:hasNext()、next()等等

ListIterator:是Iterator的子类接口,除了上面的方法还有 hasPrevious()和previous()、add()、remove()等

8.for each增强遍历

遍历数组或集合,集合只能是Collection,即list和set

9.Set接口

Set实现类:HashSetTreeSetLinkedHashSet

基本上和Collection方法一致,仅添加了少许几个方法。

注意:Set中并没有get()方法,如果想要遍历一个set,可以通过iterator或者调用toArray()生成一个数组并对数组进行遍历。

10.HashSet类

hashset内部使用了hashmap(也叫做散列表),存储的数据是无序的,不能保证存储顺序。add方法在内部是调用了hashmap的put方法。

Set类如何保证值的不重复?答案是使用map类,因为map类存储的键值对,而键是不可重复的,所以利用这一点可以实现不重复。

11.TreeSet类和Comparable接口

内部是使用TreeMap实现的。TreeSet采用了二叉树存储,是有序的。

TreeSet和TreeMap若是存储自定义的类需实现Comparable接口,否则不能使用。

集合的迭代器一般是安全失败的,TreeSet的迭代器是快速失败的。

安全失败的意思:当一个迭代器对这个集合遍历时会首先把数据拷贝一份,对拷贝数据进行迭代,这样即使在遍历过程中其他线程对这个集合进行了修改也不会出错。而快速失败就是不加拷贝,直接对集合进行遍历,但若有其他线程对集合数据进行了增删修改,就会出错。

TreeSet是有序的,排序的类需要实现Comparable<>接口并重写compareTo方法,<>中是需要排序的类型。

public class TreeSetTest {
    public static void main(String[] args) {
        TreeSet<Person> 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<Person>{
        private String name;
        private int age;
        //重写此方法自定义排序规则,
        //return值说明:负数this小,0相等,正数this大
        @Override
        public int compareTo(Person o) {
            if (age<o.age)return -1;
            else 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;
        }
    }
}

12.Map接口

Map的实现类:HashMapHashTableConcurrentHashMapTreeMapLinkedHashMap操作都是一样的。

存数据:put()

删除数据:remove(),会返回删除的这个数据,有些数据使用一次就不再使用了就可以使用remove()删除并获取

获取数据:get()

遍历数据:通过keySet()

遍历一个Map比较麻烦,可以使用keySet()获取所有键的set集合,通过遍历set中的键可以遍历所有值。

其他方法:containsKey()、containsValue()、size()、

values()将所有的值转换成collection集合,用的非常少。

13.HashMap类

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;
            }
        }
    }
    
    
14.HashMapHashTableConcurrentHashMap区别

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wqjA711i-1630894489752)(C:\Users\12416\AppData\Roaming\Typora\typora-user-images\image-20210830190729959.png)]

TreeMap不保证存储顺序但会对元素进行排序(根据键排序),LinkedHashMap可以保证存储顺序。实现原理是同时使用HashMap和双向链表保存数据,双向链表保证了存储顺序。

15.JDK9集合新特性

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);
        }
    }

2.

1.实现一个单链表

在这个实现中,使用了泛型,实现了添加数据、指定位置添加数据、删除指定位置数据、获取指定位置数据、获取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;
    }
}

2.二叉树的遍历

遍历二叉树通常有两种方式:广度优先搜索(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;
    }
}

3.构建二叉排序树

上面通过手动构建了一棵二叉树,二叉树相较于链表最大的优势就在于二叉树可以对数据进行排序,一棵平衡的有序二叉树查找元素的效率非常高,性能接近于二分查找,所以如何构建有序二叉树呢?

有序二叉树的中序输出即为排好序的数据

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);
}
4.IO

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());
                }
            }
        }
    }
}
4.1相对路径

在java中相对路径根目录是项目目录。

例如:

 File a = new File("a.txt");
 System.out.println(a.getAbsolutePath());
//在Test项目中,打印结果如下:

在这里插入图片描述

5.IO流

IO流分类:

1.按照流的方向:输入流和输出流

2.按照流动的数据类型分类:字节流和字符流

注意:字符流也来自字节流,只不过字符流对字节流进行了一些处理,

一般使用字节流较多,但通过字节流读取文字可能会乱码,所以读取文字一般使用字符流。

任何流在传输时都是二进制

顶级父类如下:

  • 字节流
    • 输入流:InputStream
    • 输出流:OutputStream
  • 字符流:
    • 输入流:Reader
    • 输出流:Writer
6.OutputStream输出流

任何流,在使用完之后都应该尽可能早的调用close()关闭流

有一个例子:有时候我们要关闭某个文件却关不掉,原因就是有其他某个进程在读取这个文件没有关闭流。

常用方法:close()、flush()刷新输出流并强制写出缓冲区、write()写入输出流

输出流用的最多的就是FileOutputStream

6.1FileOutputStream

常用方法:构造方法、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);

三种输出方式:

在这里插入图片描述

7.InputStream输出流

InputStream中最常用的子类就是FileInputStream类。

7.1FileInputStream

常用方法还是三个:构造方法、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();
    }

打印结果如下:

在这里插入图片描述

8.编码表

最初只有ASCII码,只能表示英文字母、阿拉伯数字和一些常见符号。随着发展各国语言都有自己对应的编码表,为了解决各国编码不统一的问题,出现了utf-8编码,utf-8是一种可变长的编码表。长度可以是1-4个字节。

8.1字节流读取文字的问题

由于utf-8是可变长的,所以通过字节流读取中文可能会出现读一半汉字的情况,如图:

在这里插入图片描述

如果是乱码还比较容易解决,但这种读一半文字的情况就难以解决。为了解决这个问题,所以在读取文字时都是使用字节流

9.字符流输出字符
FileWriter
    public static void main(String[] args) throws IOException {
        FileWriter fw = new FileWriter("z:\\IO\\io.txt");
        //append方法返回调用者本身,所以可以连续调用,即链式调用
        fw.append("你好!").append("张三风",0,2);
        fw.close();
    }
10.字符流读取字符
FileReader
//演示了每次读取一组字符    
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方法。字符流在读取字符后一定要刷新管道!

11.转换流(将字节流转换成字符流)–装饰者模式

假设字节流是获取到的,代码中手动创建了。

在这里插入图片描述

12.Properties

将配置输出到指定位置或者从指定位置读取配置。

    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"));
    }
13.序列化技术

序列化:将对象以文件的方式存储起来。

由于这种技术异常的简单方便,导致大量开发人员使用这种技术。随着时间的推移,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;
        }
    }
}

如果一个类中引用了其他类,那么这个其他类也需要实现序列化接口才能实现序列化。

部分属性序列化的几种方式:

  1. transient。只需在对应的属性前加上这给关键字即可
  2. static
  3. 默认方法writeObject和readObject
  4. Externalizable接口

3. 多线程

1.一些概念
    • 进程
    • 线程
    • 同步:排队执行,效率低但是安全
    • 异步:同时执行,效率高但是数据不安全
    • 并发:两个或多个事件在同一个时间段内发生,比如一天的并发量,一秒的并发量
    • 并行:两个或多个事件同时发生,同时执行。
2.实现多线程方法1

继承Thread类,并重写run方法。

每个线程都有自己的栈空间,多个线程共享一个堆空间。

3.实现多线程方法2

实现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();
4.Thread类
  1. 构造方法,常用的有三个

在这里插入图片描述

  1. 最常用的一些方法,仅列出部分:

    sleep()、start()、setPriority(int newPriority)设置优先级、setDaemon(boolean on)设置守护线程

一个线程的生命应该由线程本身决定,不能强行关闭一个线程,否则可能导致问题。如:一个线程执行一半突然被强行关闭,但线程还占用着某些资源没有释放,就会导致这些资源无法得到释放。

java早期提供一个stop方法来结束一个线程,但现在已经弃用了。

5.线程中断

通过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();
    }
6.守护线程

用户线程:例如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);
        }
    }
7.线程安全问题
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);
            }
        }
    }
8.线程同步

三种同步的方式:

前两种属于隐式锁,第三种属于显示锁

  1. 同步代码块:在一个代码块前使用synchronized关键字,格式为:synchronized () { 代码块 }
  2. 同步方法:在方法上使用synchronized关键字,锁的对象是this
  3. 显式锁:Lock l = new ReentrantLock();创建锁对象,l.lock();进行上锁l.unlock();进行解锁

公平锁和非公平锁:公平锁是先来先使用,当锁解开后先到的线程先使用;非公平锁是所有等待的线程进行抢占,抢到锁的线程先使用。

Java默认是非公平锁。如何实现公平锁?在第三种方法中,创建显示锁的时候给一个参数true,就是公平锁。

9.死锁
10.线程通信

生产者-消费者问题

11.实现多线程方法3–callable
  1. 实现callable接口,指定泛型,重写call方法。

在这里插入图片描述

  1. 创建FutureTask对象 , 并传入第一步编写的Callable类对象 FutureTask future = new FutureTask<>(callable);

  2. 通过Thread,启动线程 new Thread(future).start()

在这里插入图片描述

12.线程池

频繁创建和销毁线程非常耗时,线程池就是一个容纳多个线程的容器,池中的线程可以反复使用,省去了频繁创建和销毁线程的操作,节约了大量时间和资源。

线程池主要:包含一个线程数组和一个任务列表。

Java中四种线程池:(简略流程)

  1. 缓存线程池(不定长):如果任务来了没有空闲的线程则再创建一个线程并放入线程池去执行任务
  2. 定长线程池:如果任务来了没有空闲的线程则等待,等到有线程空闲时再执行任务
  3. 单线程线程池:线程池中只有一个线程。
  4. 周期性任务定长线程池:具有定长线程池的特点,同时还有一点:即当某个时机触发时自动执行任务。比如间隔多少秒执行一次。

Executor, ExecutorService 和 Executors区别与联系:

Executor, ExecutorService 都是接口,ExecutorService继承于Executor,Executors是工具类,他提供对ThreadPoolExecutor的封装产生ExecutorService的具体实现类。

12.1缓存线程池
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()+"拉了一裤裆");
        });
    }
}

执行结果:

在这里插入图片描述

12.2定长线程池
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()+"窗前明月光");
        });
    }
}

执行结果:

在这里插入图片描述

12.3单线程线程池
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();
    }
}

12.4周期性任务定长线程池
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();
    }
}
13.lambda表达式

使用lambda表达式注意:接口中必须只有一个方法才能使用lambda表达式。

4. 网络编程

1.一些概念

相关知识仅做粗略的解释说明,没有详细深入。

  1. ip:所有连接到计算机网络中都有一个ip地址,ip地址就像你家的地址,每个ip地址(公网ip)在全球网络中都是唯一的。

    • 公网ip:全球唯一。
    • 内网ip:局域网内部使用的ip,在所在的局域网中唯一

    ipv4:长度为32位。分为a,b,c,d,e五类,总共有42亿个左右。由于ipv4已经用尽,所以出现了ipv6,ipv6数量之多是用不完的。

    域名:相当于ip的别名,如果直接记忆ip非常不好记,通过别名可以更好的记住一个网站。

    例如:域名之于ip就像通信录中人名之于手机号。

    ip------------------域名

    手机号-----------人名

  2. 端口号:0-65535 其中0-1024是系统或一些知名软件占用了,所以应当避免使用0-1024的端口号。

    通过ip可以找到你的设备,但你的设备上运行了很多程序,那么别人发过来的数据怎么知道将数据发给哪个程序呢?例如:快递送进一个小区,如何将快递准确的送给每个人呢?是通过楼号。计算机中端口号就是这个楼号。每个程序可以占用n个端口号(通常占用1-2个就够了),通过端口号就可以知道数据分别是哪个应用程序的,从而准确的将所有数据送达相应的程序上。因此就避免了这种情况:别人给你发微信,数据传到你电脑后把数据给qq了。

  3. 协议:tcp/ip协议簇指代一系列的网络协议

    • TCP协议:三次握手建立连接,三次握手是为了确保双方收发能力都是正常的;四次挥手断开连接。
      • 可靠传输,建立连接后进行数据传输
    • UDP协议
      • 不可靠传输:不建立连接,直接发送数据,不管对方是否能收到。

网络编程程序分类:

  1. B/S程序:浏览器与服务器程序。即通过浏览器访问的服务,例如百度搜索。
  2. C/S程序:客户端与服务器程序。通过客户端访问的服务。例如qq。

B/S程序用户每次访问都是最新的服务,无需用户更新。但是B/S程序运行在浏览器中,而浏览器是别人写的,不能像C/S程序那样想怎么发数据就怎么发数据,必须按照特定的方式编写程序。因此B/S程序安全性不如C/S程序。

C/S程序用户需要手动下载新的客户端才能使用新版本,C/S程序如果被破解将难以解决,因为即使你更新了版本修复了Bug,但是用户不下载新版本也没办法。

2.TCP程序

编写C/S程序需要用到两个类:

  1. ServerSocket 搭建服务器
  2. 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("你好服务器!这是一条来自客户端的消息!");
    }
}

3.deBug技巧

在这里插入图片描述

5. XML和JSON

java可以将对象序列化,为什么还需要XML或JSON呢?

java序列化之后只有java语言可以解析序列化文件,其他语言是无法解析的。

例如:客户端使用c编写,而服务端使用java编写,客户端是识别不了java的序列化数据的。

1.XML

XML可以用于:

  1. 网络数据传输 ❌(现在基本上都是使用JSON)

    JSON解析性能比XML更高,而且数据量越大差距越明显,但XML可读性更强。XML重点用于编写配置文件。

  2. 数据存储❌(基本不用)

  3. 配置文件(XML主要用于配置文件)

    XML重点在于 掌握语法格式,解析XML了解即可。

解析XML的方式

  1. SAX解析,不必将文档全部加载到内存中,占用内存非常小,单向解析,需要程序员维护节点的父子关系。
  2. Dom解析:将整个文档加载到内存中,建立树模型,消耗资源大(缺点可忽略不计),方便操作数据和结构,通常都是使用dom解析。

此外还有Jdom和dom4j都是基于dom方式。

其中jdom包含了很多java类方便java开发,性能不是很高,而且不够灵活(因为是面向类编写的)

dom4j用有更高的性能和更高的灵活性(因为是面向接口编写的),此外还可以通过XPATH的方式解析。

Dom4j是一个非常优秀的开源的java xml api,性能优异,功能强大,易于使用。

许多开源项目都使用Dom4j来读写xml,如Hibernate

2.JSON

JSON是一种轻量级数据交换格式。

JSON(JavaScript Object Notation,JS对象简谱)是一种独立于语言的数据存储格式,是欧洲提出来的JavaScript中的一种规范。

在解析上:解析json速度比解析xml快,

在存储数据上:json表示的数据占用空间更小,更利于传输。

json出现于1999,在2005左右逐渐取代xml的地位,得益于以上一些优点。

对象格式对比:

在这里插入图片描述

Json使用频率非常之高,后端开发经常使用Json。

Java没有内置的解析Json的类,解析Json最常用的是:

  1. Gson:谷歌的
  2. 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);
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值