frankfan的胡思乱想

学海无涯,回头是岸

Java 3分钟入门

面向对象 接口 多线程 容器.数据结构 泛型

本章节我们主要从Java入门到流畅使用Java,结合我们的C++知识,提纲掣领从「面向对象」这个基本概念出发,引出一系列Java的概念和使用方式。

面向对象-封装、继承、多态

面向对象的封装以「类」为实现手段,Java中的字段(Filed)和方法(Function)以及关键字class构建了基本的封装形态-类

//Node.java
public class Node{
	protected  int value = 0;//filed;
  protected float f_value = 0.0f;
  private final String type = "Node-type";//final关键字类似于C++中的const,不过final的使用范围更广,可用于修饰类(被final修饰的类无法派生)
  
  public void setValue(int value){//function
    this.value = value;
  }
  
  public void setValue(float value){//方法重载
    this.f_value = value;
  }
  
  Node(){//构造函数
    super();//需要位于函数首行
  }
  //Java并没有析构函数的概念
  
  Node(int value){//构造函数的重载
    this.value = value;
  }
  
  //Node类的入口函数,main.
  public static void main(String[] args){
    //jvm执行node.class文件时,首先调用的函数就是main函数。
  }
}

以上就是Java的基本的类封装。

//Node.java
public class Node{
  private int value = 0;
  Node(int value){
    this.value = value;
  }
  Node(){}//如果没有提供带参的构造函数,那么编译器会自行生成默认构造函数,但是,当自定义了构造函数时,则编译器并不再提供默认构造函数。
  
  public void printValue(){
    System.out.println(this.value);
  }
}

//TNode.java
public class TNode extends Node{//java使用extends关键字作为继承
  TNode(int value){
    super(value);	
    //
    if(value){
      ///
    }else{
      ///other logic
    }
  }
}

//Main.java
public class Main{
  public static void main(String[] args){
    
    TNode tnode = new TNode(11);
    tnode.printValue();//输出11
    
		Node tnode = new TNode(12);
    tnode.printValue();//输出12;//父类引用指向子类,多态特性
  }
}

面向对象中一个重要的特性是「多态」,「父类引用(指针)指向子类,该引用(指针)有用子类特性,这被称为多态性」。

多态性之所以在面向对象中占核心重要地位,缘于其本身就是「封装」的代表。

多态性使得「金玉其外精华其中」得以实现,使用一个固定的「对象」供第三方(外界)使用,而不暴露这个固定对象后面的具体实现,这样的好处在于当需要进行重构时,供外界使用的对象并不需要进行改动,而功能(或优化)在内部「偷偷进行」,而不大动干戈。这个供外界使用的固定对象通常被称之为「接口」。

而在Java中甚至专门为此设立了一个关键字interface(接口)

interface相提并论的通常还有abstract

抽象类 abstract

//Node.java
public abstract class Node{//抽象类
  //抽象类无法被实例化(也就是并不能new一个Node对象)
  //抽象类提供抽象方法与普通方法
  private int value = 0;
  public abstract void printValue();//抽象方法并不需要(也不能)提供实现
  public void showValue(){//普通方法提供实现
    //The implementation
  }
  
  
  Node(){}//抽象类可以拥有自己的构造函数
  Node(int value){this.value = value;}//抽象类可以拥有自定义构造函数
}

//抽象类的通常目的,就是用做多态使用,抽象类无法被实例化,这就注定只能充当实例类的引用(指向某实例对象);
//需要说明的是,抽象类无法被实例化,但是却能够拥有自己的构造函数

//TNode.java
public class TNode extends Node{
  //TNode必须实现抽象类Node中的抽象方法,否则,TNode本身就成了一个抽象类(通常,这并没什么意义)
  @Override
  public void printValue(){
    //The implementation
  }
}

以上,抽象类的主要作用是体现在「多态性」的实现。

通过abstract关键字创建一个抽象类,这样的类含有抽象方法。

1、该类无法被实例化

2、抽象方法必须被子类实现(否则子类无法完成实例化)

3、抽象类可以拥有非抽象方法(这就使得其子类不必重写所有方法)

这就从语法层面对「多态」这一特性进行了进一步加强。

抽象类本质还是一个类,而Java是不支持多重继承的,因此一个子类是无法继承多个抽象类的,因此Java提供了另一个「更抽象」的概念:interface接口

接口 interface

接口是一种更「纯粹」的抽象类。

接口中只有常量而无变量,只有方法的定义而无实现(jdk8后支持default关键字,使得接口中也可以实现方法,不过需要用default修饰。目的在于使得实现接口的类不必自行实现方法,而获得默认方法的能力)

//DInterface.java
public interface DInterface {
    static final int t = 11;//接口中定义的filed必须被初始化,因为interface无法被实例化,因此变量t并不能在创建对象时初始化(因为并不能创建对象)
    public void fun1();//接口实现类必须实现该方法,否则接口实现类必须为抽象类
    public default void fun2(){//jdk8之前是不支持在接口中实现方法的,因此在jdk8之前,接口的所有实现类都必须将接口中的方法实现,否则只能成为抽象类。而jdk8之后的default关键字,提供了一种其他语言中的option功能,也就是「可选的方法实现」,接口实现类不一定要实现这个方法,这就提供了一种更大的自由度
        //The implementation
    }
    public abstract void fun3();//接口实现类必须实现该方法,否则接口实现类必须为抽象类
}

public interface Fly{
  public void fly();
  public default void birdFly(){}
  public default void duckFly(){}
  public default void chickenFly(){}
}
public interface Walk{
  public void walk();
  public default void birdWalk(){}
  public default void duckWalk(){}
  public default void chickenWalk(){}
}

//SpecialAinmal.java
public class SpecialAnimal implements Fly,Walk{
  @Override
  void duckFly(){
    //The implementation
  }
  
  @Override
  void duckWalk(){
    //The implementation
  }
}
//SpecialAnimal这个类实现了🦆走路和🦆飞的方法,但是它还是叫SpecialAnimal而非鸭子,可能后续有进化,又拥有了其他方法,但依旧还是SpecialAnimal。Java虽然不支持多重继承,但是支持同时实现多个接口,这就是「更纯粹的抽象类」- 「接口」的魅力。

本质上,interface接口,是一种「协议protocol」,事先设计好一种协议(interface),相关方都实现该协议,这样就能自由的调用与被调用。

多线程

Java的多线程被封装成线程类,使用及其简单,线程加锁也通过synchronized关键字简单高效的得以实施,此外,Java多线程也提供诸如wait notify等等函数来进行更细节灵活的操作。本章节只讲解最基本的Java线程操作。

//Work.java
public interface Work {//创建一个接口
    public abstract void job();
}

//TThread.java
public class TThread extends Thread{

    private Work work = null;
    private static String flag = "once";
    TThread(Work job){//接受外界传入的一个工作者对象
        work = job;
    }
    TThread(){}

    @Override
    public void run() {//当线程被启动(start)时,该方法被执行,在这个方法内调用work对象的job方法开始执行任务
        super.run();
        synchronized (flag){//synchronized传入一个对象,当该对象被1个线程获取时,另一个线程则无法获取该对象,在Java中,每一个对象都可以被当做同步对象,这就是java的线程锁机制,synchronized代码块只能被1个线程执行完,该线程结束,另一个线程才能回去到该同步对象,并执行代码块

 					if(work != null){
            	work.job();
          }
        }
    }
}

//Worker.java
public class Worker implements Work{
  
  @Override
  void job(){
    //do the job logic
  }
  
  public static void main(String[] args){
    Worker wk = new Worker();
    //开启2个线程执行工作
    TThread t = new TThread(wk);
    t.start();
    
    TThread t2 = new TThread(wk);
    t2.start();
  }
}

以上就是一个最简单的多线程使用场景。在番外篇中我们会更为详细的讨论Java的多线程相关内容。

IO与流

Java将IO相关接口封装得非常简单好用,整体以「流(Stream)」为世界观构建。为内存中的数据提供各种编解码方式。

」是一个非常抽象的概念,但好在如果以「二进制bit序列」的角度去尝试理解则会具象很多。一切数据,无论存在于文件,还是内存,亦或是其他输入输出设备甚至远程主机(网络),本质都能被抽象成「一串」二进制序列,也就是我们所谓的「Stram流」。

「水往低处流」,流,具有方向性。通常以「程序」为起点,「设备」为终点,确定为流的方向。

内存中数据(程序数据)流向设备(如写文件),称之为「输出流」,向屏幕上输出内容(内存数据流向设备),同样被称之为「输出流」。

而设备数据流向程序(内存),则被称之为「输入流」,如读取文件,如从远处设备(网络)读取数据等等。

除了「输入输出流」「字节流字符流」这种见名知意的所谓「流类型」外,还需要从逻辑上理解一种流类型:「节点流」「过滤流」。

当然,在java中并不存在真正的概念或者关键字用来表示所谓节点流或者过滤流,这只是从逻辑上而言的

  • 节点流: 直接的操作IO设备,这种流就是节点流。
  • 过滤流: 对节点流的包装,这种流类型提供更强大的「附加」功能
//File.java
public class File {
    public static void main(String[] args)  {
        try {
						
          	//创建一个节点流,非常简单
            FileOutputStream outstream = new FileOutputStream("file.txt");
            byte[] bytes = new byte[111];
            for (int i = 0;i<bytes.length;i++){
                bytes[i] = Byte.parseByte(String.format("%d",i));
            }
          	//创建节点流后,直接往磁盘文件中写入内容
            outstream.write(bytes);

        }catch (IOException e){
        }
    }
}

以上就是一个节点流的基本用法,但是这样似乎效率不高,我们可以更近一步,使用过滤流,给这个节点流添加一个缓存buf,提高IO效率

//File.java
public class File {
    public static void main(String[] args)  {
        try {
          	//创建一个单节点,这个file节点既不能直接写入字符串,也没有buf功能
            FileOutputStream outstream = new FileOutputStream("file.txt");
					 //创建一个buf过滤节点,用来包装这个单节点,使得这个单节点拥有buf功能,IO效率更高
            BufferedOutputStream bufOutStream = new BufferedOutputStream(outstream);
          	//buf这个过滤节点包装后,这个流依然不具备直接写入字符串的能力,需要再用一个输出写入流进行包装
            OutputStreamWriter outwrite = new OutputStreamWriter(bufOutStream);
          	//最后,这个流就具备了,文件读取,buf缓存以及直接写入字符串的功能。通过这种「搭积木」的构建方式,能够自由灵活的构建所需要的完整功能,这也是unix哲学中的「功能最小化」世界观。
					 outwrite.flush();
            outwrite.write("hello world");
            outwrite.close();
          
          /*
          当然,在实际中,相较于OutputStreamWriter,我们更多使用的是DataOutputStream,根据其名可知,这是一个专注数据处理(输入输出)的过滤流,实际上,这个类提供了各种方便快捷的数据写入方式
            DataOutputStream dataout = new DataOutputStream(bufOutStream);
            dataout.writeBytes("opps");//直接写入字节流
            dataout.flush();
            dataout.close();
          */
          
        }catch (IOException e){
            e.printStackTrace();
        }
    }
}

Java中的容器(数据结构)

「容器」是每个面向对象编程语言的编程利器,C++中有STL(尽管饱受诟病),Java中有一系列自带的类库,均以面向对象的方式进行封装,简单好用。

  • Collection
    • List
      • ArrayList
      • LinkedList
      • Vector
      • Stack
    • Set
      • HashSet (还有其他,这个最常用)
  • Map
    • HashMap
      • HashMap(还有其他,这个最常用)
    • TreeMap
      • Hashtable(还有其他,这个最常用)

这就是Java中的两类数据容器。

最常见的数据结构是List Set Map这3类。每一类中都有具体的容器类,在同一个系列下,不同的容器类源于其底层实现的数据结构的不同。

这些容器之间几乎使用相同的面向对象接口,决定这些容器的不同使用方式通常指取决于2点

1、业务需求是什么(是单纯的元素存储还是键值对方式)

2、结合业务需求选用最高效(结构合适)的那个容器

本章节只简单的讨论我们常用的集中容器类型

  • 「动态数组类型」
  • 「字典类型」
  • 「集合类型」

这三种也是实际场景中最为常见和常用的容器类型

  • 动态数组

    ArrayList,这个动态数组底层是基于连续内存分配的方式进行实现的,因此拥有极高的随机访问和遍历特性,但插入和删除效率较低。

    //DataContainer.java
    public class DataContainer {
        public static void main(String[] args)  {
          //内存连续的动态数组,Java的数据容器只支持「对象」,而不支持「原始(primitive)」类型,不过好在Java提供primitive的「装箱」操作,也就是提供一个对象类型,可以无缝的用来包装primitive类型数据。比如Integer v = 11;这里的v是一个Java的对象,而Integer是一个Java类,专门用来包装int这个原始类型的,我们知道每一个Java对象的创建都是通过new来进行的,而Java为了原始类型与对象类型能够「看起来」无缝装箱,因此从语法设计上支持Integer value = 1024这样的操作,但并不意味着value是一个primitive
        	ArrayList<Integer> nums = new ArrayList<>();
          for (int i=0;i<100;i++){
                nums.add(i);
           }
            nums.set(0, 1024);
            int v = nums.get(0);
            System.out.println(v);//output:1024
            System.out.println(nums);//output:[1024,1,2,3...]
            nums.removeAll(nums);
            System.out.println("after removed:"+nums);//output:[]
            nums.add(101);
            int lastone =  nums.get(nums.size()-1);
            System.out.println(lastone);//output:101
        }
      	//ArrayList是内存连续的结构,因此对于数据的访问和遍历是非常高效的,但是增加和删除效率都比较底下
    }
    

    LinkedList这个动态数组底层是基于双向链表的方式进行实现的,因此拥有极高的插入删除特性,但随机访问和遍历效率低下。

    //DataContainer.java
    public class DataContainer {
        public static void main(String[] args)  {
    				
            //基于双向链表实现的动态数组,这个类也实现了List接口,因此从操作方法上与ArrayList无异,不过因为其基于链表的实现原理,该类也实现了Deque接口,因此也支持push、pop、addLast、addFirst等链表特性的方法,因此,若无需进行频繁的随机访问,而又需要频繁的插入删除操作,则可选择LinkedList容器
            LinkedList<Integer> list = new LinkedList<>();
            list.add(11);
            list.add(22);
            System.out.println(list);
    
            list.remove();//移除首个元素
            System.out.println(list);
            list.push(33);//提供栈的push方法
            list.push(44);
            System.out.println(list);
            list.pop();
            System.out.println(list);
        }
    }
    

    此外Java也提供vector这样的动态数组,与其他不同的是这是 一个线程安全的动态数组,但效率很低,因此现在实际中使用并不太多了。

  • 字典类型

    Map就是Java中的所谓字典类型,不过这是一个接口,其实现最为经典古老的类是HashTable ,这是一个线程安全类,不过官方逐渐放弃对其维护,因此现在最为常用的是「HashMap

    //DataContainerMap.java
    public class DataContainerMap {
        public static void main(String[] args)  {
    			 //数据放在内存中,其实无非不是只有2种情形,1、一段连续的内存,2、一段非连续的内存。一段连续的内存只需要知道这段内存的起始地址以及结束(长度或者某个标志)就能进行管理,而另外一种类型是离散的内存,对离散内存的管理无疑是比连续内存管理复杂度更高更难的,但它的意义很大,在于既能够充分利用碎片化的内存,也能够发挥高效数据插入删除的优势,「管理离散内存」的方案大体上是2个方向,1、表,2、树。链表本质是一种特殊的树,而Map容器就是「表」的管理方式。
          
            //所谓表,记录的是一种「A数据与B数据之间的关系」,这些关系通通被记录在表中,在表中A数据通常被称为字段(key),而B数据则通常被称为值(value),通常一个表中会记录N对这样的关系对,本质上这些关系对没有顺序前后关系,但可以从逻辑上进行排序(比如根据字段的某些特性如ASCII码等)。
          	//在Java中,一对这样的关系被称为一个实体(Entry)
            HashMap<String,Object> map = new HashMap<>();
            map.put("name","zs");//创建一个实体(entry)
            map.put("age",11);
            map.put("height",123.0f);
    
            for (String key:map.keySet()){//遍历这个表中所有实体的字段值(key)
                System.out.println(key);
            }
    			
            //遍历这个表中所有的entry,一个entry拥有key,和value
            for (Map.Entry<String,Object> enter:map.entrySet()){
                System.out.println(enter.getValue());
            }
    			 
           //一个表中所有的字段(key)和值(value)分别构成一个「集合set」,set也是一种数据容器,一种离散的内存数据结构,Java中的HashSet本质就是HashMap,set的底层数据结构就是一个HashMap,调用set结构存放object时,本质就是在往HashMap中存放数据,不过放入set中的对象在底层被用作HashMap的key,如此而已。
          //set的用处在于极高(O(1))的访问速度,以及不允许重复元素存在的特性,这在很多场合中带来便利,去重,就是一项基本的功能。
        }
    }
    

    以上,就是本章节的基本内容,此外,Java还有很多其他的概念和功能,这部分内容在番外篇中讲解。

posted on 2021-12-28 00:10  shadow_fan  阅读(25)  评论(0编辑  收藏  举报

导航