• 《practical Java》读书笔记


    题记:

    花了一周把Peter Haggar的《practical Java》看了遍,有所感悟,年纪大了,

    写下笔记,方便日后查看.也希望有缘之人可以看看,做个渺小的指路人。

    不足之处还望指正。

    概述:

    全书分为六个部分,包括一般技术、对象与相等性、异常处理、性能、多线程、对象。

    一般技术:举例了几个java常见错误用法的说明和解释,诸如array和vector的选择,多态与instanceof等等

    对象和相等性则:针对equals的详细说明,是迄今本人见过对equals理解最深的一本书了,其中不乏java的一些规范

    异常处理:主要介绍了java异常机制的使用细节,其中有一点就是return后的逻辑一律不执行在try finally模式里头是无效的

    性能:介绍了java常用的一些优化细节,诸如使用栈变量来代替堆变量,减少同步化,使用arraycopy方法来代替自己的循环复制数组等等

    多线程:简要的说明了java线程使用中一些常见的知识点,如果对java多线程有兴趣的,可以看看《Java并发编程实战》

    对象:介绍了接口与继承的关系与使用,对深入学习java框架源码有一定的帮助,感兴趣的可以多思考下其中的奥义

    正题:

    正题中将挑选自己觉得比较有用的一些知识点进行说明,但并不代表其他知识点就不重要(因人而异,尽信书 不如无书)

    最后我会将代码工程打包好,感兴趣的可以去下载下来看看。

    1.一般技术

    实践1:参数以by value 方式而非by reference 方式传递

     1 /**
     2  * java有值(基础类型变量value)传递和引用(reference)传递
     3  * 两者的区别是引用将会随着调用方法体的逻辑而发生改变
     4  * 一个非常基础的java知识点
     5  * 献给千千万万徘徊在java门口的求学者
     6  * @author lwx
     7  * TODO
     8  * 参考:
     9  * 2014-5-13 上午9:28:38
    10  */
    11 public class Lesson1 {
    12 
    13     public static void  valueTest(int value){
    14         
    15         value=value+5;
    16         System.out.println("valueTest-->"+value);
    17     }
    18     
    19     
    20     public static void referenceTest(StringBuffer obj){
    21         
    22         obj.append("123");
    23         
    24         System.out.println("referenceTest-->"+obj.toString());
    25     }
    26     
    27     public static void main(String[] args) {
    28         int value=2;
    29         
    30         valueTest(value);
    31         System.out.println(value);//2
    32         StringBuffer obj =new StringBuffer("0");
    33         referenceTest(obj);
    34         System.out.println(obj.toString());//0123
    35         
    36     }
    37 }
    View Code

    实践2、3:final的用法 略

    实践4:在arrays和vectors之间慎重选择

    数组在java中使用率远远超过vector,因此这里就要明白何时应该使用vector

    首先vector是线程安全的,这样就保证了他可以同步控制对象,

    其次vector内部实现也是数组,只不过是泛型对象的数组(数组更多时候存储的是基础类型)

    最后就是数组的大小是无法自行拓展的,而vector是可以通过System.arraycopy()方法进行复制扩展vector的容量

    实践5:多态优于instanceof

    这里必须明白java多态和instanceof用法,简单说下两者的概念

    所谓的多态就是不同对象对同一个消息作出不同的响应,java中的多态包含了重载和重写(覆盖)

    instanceof 运算符是用来在运行时指出对象是否是特定类的一个实例,通过返回一个布尔值来指出,这个对象是否是这个特定类或者是它的子类的一个

    先看下面一个例子

     1 /**
     2  * 尽量使用多态而非instanceof
     3  * @author lwx
     4  * TODO
     5  * 参考:
     6  * 2014-5-13 上午10:39:41
     7  */
     8 public class Lesson5 {
     9     public static void main(String[] args) {
    10         Employee mgr=new Manager();
    11         Employee pgr=new Programmer();
    12         System.out.println("经理的工资-->"+calcSalary(mgr));
    13         System.out.println("程序员的工资-->"+calcSalary(pgr));
    14     }
    15     
    16     public static int calcSalary(Employee e){
    17         int salary=e.salary();
    18         if(e instanceof Programmer)
    19             salary+=((Programmer)e).bonus();
    20         return salary;
    21     }
    22 }
    23 interface  Employee{
    24     public int salary();
    25 }
    26 class Manager implements Employee{
    27 private static final int mgrSal=10000;
    28     @Override
    29     public int salary() {
    30         // TODO Auto-generated method stub
    31         return mgrSal;
    32     }
    33 }
    34 class Programmer implements Employee{
    35     private static final int pgrSal=6500;
    36     private static final int pgrBonus=1000;
    37     @Override
    38     public int salary() {
    39         // TODO Auto-generated method stub
    40         return pgrSal;
    41     }
    42     //程序员除了工资 还有项目奖金哦 那个公司有 求收留
    43     public int bonus() {
    44         // TODO Auto-generated method stub
    45         return pgrBonus;
    46     }
    47 }

    上面例子中 程序员在计算工资的时候是需要考虑奖金的,因此通过Instanceof来判定传给

    calcSalary方法的参数是否是Programmer类,如果是 则在原有工资计算方法上加上bonus()方法
    表面上看,这样的逻辑没有问题,但是我们是需要考虑拓展的,加入现在还有产品经理,他的工资也有奖金 另外还有其他福利,在加上其他岗位 那么每增加一个岗位的变动
    我们都需要去修改calcSalary方法,而这样的设计明显是不符合java的规范的
    书本作者给出的方案是让经理也有奖金的方法,这不过这个奖金是0 从而避免了instanceof的产生,具体做法看书本,此处略
    实践6:必要时才需要instanceof
    java支持父类向下转型,即使是错误的向下转型 在编译的时候是不会报错的,因此容易让开发人员带来干扰

     1 /**
     2  * 必要时才用instanceof
     3  * 必要的时候指的是 你需要父类向下转子类
     4  * @author lwx
     5  * TODO
     6  * 参考:
     7  * 2014-5-13 上午11:18:32
     8  */
     9 public class Lesson6 {
    10     
    11     public static void main(String[] args) {
    12         
    13         Shape circle=new Lesson6.Circle();
    14         Object triangle=new Lesson6.Triangle();
    15         
    16         //Lesson6.Triangle tri1=(Lesson6.Triangle )circle;//编译通过 但是执行会报错  java.lang.ClassCastException
    17         if(circle instanceof Lesson6.Triangle){
    18             
    19             Lesson6.Triangle tri1=(Lesson6.Triangle )circle;
    20         }
    21         Lesson6.Triangle tri2=(Lesson6.Triangle )triangle;
    22     }
    23     
    24     static class   Shape{}
    25      static    class Circle extends Shape{}
    26      static class Triangle extends Shape{}
    27 
    28 }

    实践7:一旦不再需要object reference,就将它设为null

    接触java的都明白java自带的虚拟机有垃圾回收机制,不愿太操心内存问题,其实作为一名合格的javaer也是需要考虑内存泄露的

    况且java确实有存在,当然这里不再我们的讨论话题中,为什么没用的引用尽量要手动的去触发unusefulObj=null呢

    其实就是减轻JVM的工作量,gc不是随时触发的 这个应该要懂得

    实例中的例子已经很不错了

     1 /**
     2  * 手动去设置无用的引用为null
     3  * @author lwx
     4  * TODO
     5  * 参考:
     6  * 2014-5-13 下午1:56:03
     7  */
     8 public class Lesson7 {
     9     
    10     
    11     
    12     public static void main(String[] args) {
    13         
    14         //testGC();//GC测试CPU性能
    15         
    16         Customers customers=new Customers("");
    17         
    18         //执行一堆逻辑  此处省略
    19         
    20         /*
    21         1.
    22         无用时候释放对象
    23         customers=null;//
    24          */    
    25         
    26         
    27         /*2.
    28          * 上面的情况 存在一个问题 假如customers我们还需要 
    29          * 只是他的数据我们不需要引用了 或则customers生命周期跟系统应用一个周期
    30          * 那么我们就只需要释放custIdArray内存就可以达到效果了 
    31          * */
    32         //由于我们没有办法直接接触 因此需要开放方法给我们去触发 加入一个unrefCust()方法
    33         customers.unrefCust();//
    34         
    35     }
    36 
    37     private static void testGC() {
    38         Runtime rt=Runtime.getRuntime();
    39         long mem=rt.freeMemory();
    40         System.out.println("空闲CUP==>"+mem/1024/1024);
    41         System.gc();//手动去触发虚拟机回收垃圾  
    42         mem=rt.freeMemory();
    43         System.out.println("忙时CUP==>"+mem/1024/1024);
    44     }
    45 
    46 }
    47 
    48 class  Customers{
    49     
    50     private int []custIdArray;
    51     
    52     public Customers(String db){
    53         int num=queryDB(db);
    54         custIdArray=new int[num];
    55         for (int i = 0; i < num; i++) {
    56             custIdArray[i]=i;
    57         }
    58     }
    59     int queryDB(String sql){
    60         
    61         return 5;
    62     }
    63     
    64     public void unrefCust(){
    65         
    66         custIdArray=null;
    67     }
    68     
    69     
    70 }
    View Code

    2.对象与相等性

    实践8:区别reference类别和primitive型别

    这个可能比较拗口很难理解字面的意思,其实也是实践1所说的基础类型和对象引用之间的区别

    这里主要是介绍下java1.5的一个新特性:拆箱和装箱

    Integer i1=100;//等同于new Integer(100)  这里就是一个自动装箱的过程

    int k=i1;// 自动拆箱的过程  

    更多关于拆箱与装箱 可以移步http://www.cnblogs.com/danne823/archive/2011/04/22/2025332.html

    关于包装类的缓存:http://blog.csdn.net/yaoweijq/article/details/6021706

    /**
     * 基础类型和引用类型对象区别
     * 1.5新特性  装箱和拆箱
     * @author lwx
     * TODO
     * 参考:
     * 2014-5-13 下午2:15:45
     */
    public class Lesson8 {
        
        public static void main(String[] args) {
            
            //八大基础类型和对应的装箱类
        /*    boolean char byte short int float long double
    
            对应的对象为
    
            Boolean Character Byte Short Integer Float Long Double*/
            
            Integer i1=100;//等同于new Integer(100)
            Integer i2=100;
            System.out.println(i1==i2);//true
            i1=1000;
            i2=1000;
            System.out.println(i1==i2);//false
            
        
        }
    
    }

    实践9:区分==和equals

    这个是java经常碰到的一个基础知识点,即"=="和"equals"区别,何时使用==何时使用equals

    总结起来可以这么说:== 对于基础类型 比较的是vlaue,而引用类型比较的是地址,当对象不需要单纯的比较地址

    而需要你自己DIY的时候,请重写equals方法吧

    至于何时使用,可以这么说:==经常是基础类型在用,引用类型的基本不用

    equals最常见,而且多数情况下你是需要重写的

    有点以偏概全,希望拍砖

    实践10:不要依赖equals()的缺省实现

    不啰嗦了,直接上代码

     1 /**
     2  * 
     3  * 重写父类equals方法(默认重写的是Object的equals方法)
     4  * @author lwx
     5  * TODO
     6  * 参考:
     7  * 2014-5-13 下午3:46:15
     8  */
     9 public class Lesson10 {
    10     
    11     public static void main(String[] args) {
    12         
    13         
    14         BasketBall b1=new BasketBall("brand",20.0);
    15         BasketBall b2=new BasketBall("brand",20.0);
    16         System.out.println(b1.equals(b2));//不重写 则调用Object equals的方法
    17     }
    18     
    19 
    20 }
    21 
    22 
    23 class  BasketBall{
    24     
    25     private String brand;
    26     
    27     private double price;
    28     
    29     public BasketBall(){}
    30     
    31 
    32     public BasketBall(String brand, double price) {
    33         super();
    34         this.brand = brand;
    35         this.price = price;
    36     }
    37 
    38     @Override
    39     public boolean equals(Object obj) {
    40         
    41         if(null!=obj&&obj.getClass()==getClass()){
    42             
    43             BasketBall ball=    (BasketBall) obj;
    44             if(brand.equals(ball.brand)&&price==ball.price){
    45                 
    46                 return  true;
    47             }
    48         }
    49         return false;
    50     }
    51     
    52 }
    View Code

    上面算是比较正常的一个重写equals的方法,后续书本作者也提到了一个情况

    就是对象的变量如果不是基础类型,也是引用类型的话,就需要额外处理了(举个例子,比如brand改成Stringbuffer类型)

    作者给出了四个解决办法:

    1.不使用Stringbuffer,继续使用String

    2.比较的时候 先将Stringbuffer对象转出String(调用toString()方法)

    3.继承Stringbuffer,重写equals方法 让变量变成重写类的类型(有点拗口)

    4.放弃equals改用compare()方法

    例子书本上都有,感兴趣的都可以去看看

    关于何时重写equals,上一节已经表述了自己的观点,这里补充下原作者的观点:

    实践11~15 略  

    3.异常处理

    实践16:认识【异常控制流】机制

    记住一个模式:try { //do something }catch(Exception e){// when exception happen  to do }finally{//不管有无异常 都将执行 不受return 影响}

    顺序为:先执行try中的逻辑,如果正常执行,则跳转到finally块中执行,如果异常了,则会终止try块中的逻辑,转移到

    catch块中执行,最后还是会在finally完成最后的操作

     1 /**
     2  * @author lwx
     3  * TODO
     4  * 参考:
     5  * 2014-5-13 下午4:40:36
     6  */
     7 public class Lesson16 {
     8     
     9     
    10     public static void main(String[] args) {
    11         
    12         int i =0;
    13         int k=2;
    14         int addResult=0;
    15         int divideResult=0;
    16         try {
    17             divideResult=k/i;
    18             addResult=k+i;//这里将不会执行
    19         } catch (Exception e) {
    20             System.out.println("异常了-->"+e.getMessage());
    21         }finally{
    22             
    23             addResult=1;
    24             divideResult=0;
    25         }
    26         System.out.println("addResult-->"+addResult);
    27         System.out.println("divideResult-->"+divideResult);
    28         
    29     }
    30     
    31 
    32 }
    View Code

    实践17:绝对不可轻视异常

    当发生程序异常的时候,我们有哪些处理方式

    1.捕获并处理,防止它进一步传播

    2.捕获并在此抛出它,传播给它的调用者 

    3.捕获它,并抛出一个新的异常给调用者

    4.不捕获这个异常,任由它传播

    这里需要用到引用一个新的概念:抛出异常,通过throws来完成

    正常情况下,第一个处理方式是最常见的,实践16中也是采用了第一种处理方式

    后续三种我们将在实践18~20中一一介绍

     1 public class Lesson17 {
     2     public static void main(String[] args) {
     3         try {
     4             test();
     5         } catch (Exception e) {
     6             // TODO Auto-generated catch block
     7             e.printStackTrace();
     8             System.out.println("异常处理提示");
     9         }
    10     }
    11     
    12     static void test () throws Exception{
    13         
    14         System.out.println(2/0);
    15         
    16         throw new Exception();
    17         
    18     }
    19 
    20 }
    View Code

    实践18:千万不要遮掩异常

    在处理try块汇总的异常时,如果catch获取finally中又抛出异常,那么之前的异常会被覆盖

    优先级:finally>catch>try

    我们知道,finally是不受之前是否异常影响的,都将会执行,但是特殊情况下finally语句照样

    也会产生异常,那到底要如何处理呢?那就是将异常存放在Vector中

      1 /**
      2  * 
      3  * 如何捕获所有的异常 -->将异常对象存放在容器对象中
      4  * 
      5  * @author lwx TODO 参考: 2014-5-13 下午5:02:51
      6  */
      7 public class Lesson18 {
      8 
      9     public static void main(String[] args) {
     10         
     11         Hidden hidden = new Hidden();
     12         try {
     13             hidden.readFile();
     14         } catch (FileNotFoundException e) {
     15             // TODO Auto-generated catch block
     16             e.printStackTrace();
     17         } catch (IOException e) {
     18             // TODO Auto-generated catch block
     19             e.printStackTrace();
     20         }
     21         //改进过的捕获异常的方式
     22         NotHidden notHidden=new NotHidden();
     23         
     24         try {
     25             notHidden.readFile();
     26         } catch (ReadFileException e) {
     27             // TODO Auto-generated catch block
     28             e.printStackTrace();
     29             //捕获到的异常存放到容器中
     30             System.out.println(e.exceptionVector().size());
     31             
     32         }
     33     }
     34 
     35 }
     36 
     37 class ReadFileException extends IOException {
     38     private Vector excVector;
     39 
     40     public ReadFileException(Vector v) {
     41         excVector = v;
     42 
     43     }
     44 
     45     public Vector exceptionVector() {
     46 
     47         return excVector;
     48     }
     49 
     50 }
     51 
     52 class Hidden {
     53 
     54     void readFile() throws FileNotFoundException, IOException {
     55 
     56         BufferedReader br1 = null;
     57         BufferedReader br2 = null;
     58         FileReader fr = null;
     59 
     60         try {
     61             fr = new FileReader("test.txt");
     62             br1 = new BufferedReader(fr);
     63             int i = br1.read();
     64 
     65             fr = new FileReader("test2.txt");
     66             br2 = new BufferedReader(fr);
     67             i = br2.read();
     68 
     69         } finally {
     70 
     71             if (null != br1) {
     72 
     73                 br1.close();
     74             }
     75             if (null != br2) {
     76 
     77                 br2.close();
     78             }
     79 
     80         }
     81 
     82     }
     83 
     84 }
     85 
     86 class NotHidden {
     87 
     88     void readFile() throws ReadFileException {
     89 
     90         BufferedReader br1 = null;
     91         BufferedReader br2 = null;
     92         FileReader fr = null;
     93         Vector excVec = new Vector(2);
     94 
     95         try {
     96             fr = new FileReader("test.txt");
     97             br1 = new BufferedReader(fr);
     98             int i = br1.read();
     99 
    100             fr = new FileReader("test2.txt");
    101             br2 = new BufferedReader(fr);
    102             i = br2.read();
    103         } catch (FileNotFoundException e) {
    104             // TODO Auto-generated catch block
    105             e.printStackTrace();
    106             excVec.add(e);
    107         } catch (IOException e) {
    108             // TODO Auto-generated catch block
    109             e.printStackTrace();
    110             excVec.add(e);
    111         } finally {
    112 
    113             if (null != br1) {
    114                 try {
    115                     br1.close();
    116                 } catch (IOException e) {
    117                     // TODO Auto-generated catch block
    118                     e.printStackTrace();
    119                     excVec.add(e);
    120                 }
    121             }
    122             if (null != br2) {
    123 
    124                 try {
    125                     br2.close();
    126                 } catch (IOException e) {
    127                     // TODO Auto-generated catch block
    128                     e.printStackTrace();
    129                     excVec.add(e);
    130                 }
    131             }
    132             if(excVec.size()>0){
    133                 throw new ReadFileException(excVec);
    134             }
    135         }
    136 
    137     }
    138 
    139 }
    View Code

    实践19:明确throws字据的缺点

    我们在开发过程中,经常会调用一些公用的函数(作者给了一个很通俗的名称:工蜂型函数),而这些函数

    有可能会产生异常(比如调用数据库连接的方法),这时候处理异常有两种方式

    一种是函数自身处理,一个是调用端来处理.个人偏好是函数本身来处理,否则有10处地方调用这个函数

    就要捕获10次

    实践20:细致而全面的理解throws子句

    看的不是很懂,但是根据作者的demo,我理解的是作者想表达的意图是当重写函数的时候

    函数抛出的异常范围不能大于该函数抛出范围,当然也可以不抛出异常(子类抛出异常的范围不能大于父类)

     1 /**
     2  * 
     3  * 重写父类带有异常的方法
     4  * 则子类中的方法抛出的异常必须是该异常或则该异常的父类(抛出异常范围更大)
     5  * 当然 也可以不抛出异常
     6  * @author lwx
     7  * TODO
     8  * 参考:
     9  * 2014-5-13 下午7:02:57
    10  */
    11 public class Lesson20 {
    12 
    13     
    14     public static void main(String[] args) throws IOException {
    15         
    16         
    17         ChildClass childClass =new ChildClass();
    18         childClass.test();
    19         childClass.test2();
    20         childClass.test3();
    21     }
    22 }
    23 
    24 
    25 class SubClass{
    26     
    27     
    28     
    29     public void test ()throws FileNotFoundException{
    30         
    31     }
    32     public void test2 ()throws FileNotFoundException{
    33         
    34     }
    35     public void test3 ()throws FileNotFoundException{
    36         
    37     }
    38     
    39     
    40 }
    41 class ChildClass{
    42     
    43     
    44     
    45     public void test ()throws FileNotFoundException{
    46         
    47     }
    48 public void test2 ()throws IOException{
    49         
    50     }
    51     public void test3 (){
    52         
    53     }
    54 }
    View Code

    实践21:使用finally避免资源泄露

    简单的描述就是在异常处理中,java规范是通过finally来做一些善后的事情(包括释放资源等)

    实践23:将try/catch区段置于循环之外

    弦外音:不能循环调用try/catch区段,而应该在一个try/catch中调用循环

    实践27:抛出异常前线将对象恢复为有效状态

    弦外音:将状态变量等处理放在可能处理异常之后,保证状态不受异常影响

    4.性能

    实践31:如欲进行字符串结合,StringBuffer优于String

    弦外音:对于需要频繁处理字符拼接组合的地方,请使用StringBuffer,或则对于稍微复杂的操作,请使用StringBuffer

    实践33:慎防未使用的对象

    弦外音:如果一个判断两选一,请不要都创建之,然后根据if else 来选择其中一个对象

       换句话说,使用的时候才去创建对象

    实践34:将同步化降至最低

    弦外音:尽量少用同步操作,诸如synchronized等,除非你需要同步资源

    实践35:尽可能使用stack变量

    弦外音:尽量使用局部变量(stack)来代替全局变量(heap)

    实践36:使用static,final和Privatae函数以促成inlining

    略,后续在认真看

    5.多线程

    略,对多线程感兴趣的朋友可以看看,都比较基础

    6.对象

    实践59.运用接口来支持多继承

    弦外音:java虽然不支持多继承(class a extends b,c)但是却可以通过接口

    来完成多实现(class a implements a,b ,c......)

    实践60.避免接口函数发生冲突

    弦外音:假如类A实现了B,C两个接口,但是B和C都有方法test()

    这样A要实现B还是C的test()方法呢,作者给出了解决方式,个人觉得方法很赞

    java中很多库都使用了类似的方式来解决这类问题

    即让D继承B接口(重命名接口,避免冲突),然后class A Implements D,C

    实践61、62:

    关于继承和接口 抽象类等知识点 

    实践63~66

    关于类引用之间的操作 包括浅克隆 深克隆等

    实践68:在构造函数内调用non-final函数是要当心

    这个记得初学java时候,比较难搞懂的一道题目,了解其机制

    需要明白java对一个类的初始过程

    我们结合代码来看下

     1 /**
     2  * 构建子类构造的时候会调用父类的构造函数
     3  * 而父类的构造函数中有调用lookup() 注意 此时的loopup()调用的是子类的loopup()
     4  * 而此时子类中的变量都还没初始化 都是默认值 因此num=0调用之后val也是0
     5  * 下面打印的语句顺序可以参考 就明白了
     6  * @author lwx
     7  * @TODO 参考: 
     8  * @createtime 2014-5-13 下午7:44:35
     9  */
    10 public class Lesson68 {
    11     
    12     public static void main(String[] args) {
    13         
    14         Derived derived=new Derived();
    15         System.out.println(derived.value());
    16         
    17         
    18     }
    19 
    20 }
    21 
    22 class Base {
    23 
    24     private int val;
    25 
    26     public Base() {
    27         System.out.println(" Base()");
    28         val = lookup();
    29     }
    30 
    31     public int lookup() {
    32         System.out.println(" Base lookup");
    33         // TODO Auto-generated method stub
    34         return 5;
    35     }
    36 
    37     public int value() {
    38         System.out.println(" Base value");
    39         return val;
    40     }
    41 }
    42 
    43 class Derived extends Base{
    44     private int num=10;
    45     public int lookup(){
    46         System.out.println(" Derived lookup");
    47         System.out.println(" Derived lookup num=="+num);
    48         return num;
    49         
    50     }
    51     public Derived(){
    52         System.out.println(" Derived()");
    53         System.out.println(" Derived Derived() num=="+num);
    54     }
    55     
    56 }

    PDF和笔记源码下载地址:http://download.csdn.net/detail/draem0507/7342879

       

  • 相关阅读:
    1.12学习总结:分区
    1.11学习总结:持久化
    1.10学习总结:RDD的行动操作
    1.9学习总结:RDD的转换操作
    1.8学习总结:RDD创建
    1.7学习总结:pyspark实例WordCount
    1.6学习总结:Spark集群的高可用配置
    1.5学习总结:安装Spark
    毕业设计第四周第七天完成情况汇总
    毕业设计第四周第五天完成情况汇总
  • 原文地址:https://www.cnblogs.com/draem0507/p/3726333.html
Copyright © 2020-2023  润新知