实际工作项目开发之最少必知 Java 基础知识

本文详细讲解了二进制基础、数据类型的选择与操作(包括值类型、引用类型、字符串、数组和对象),代码执行流程,以及面向过程和面向对象编程的区别,以及进程、线程和乐观/悲观锁的概念。

一、二进制

  • 我们写的程序语言最后都要跑在 CPU 里面,而 CPU 只认识机器码(二进制)
  • 计算机是用二进制保存所有数据
  • 但是我们写代码的时候不可能全部用 0101 这样的二进制的方式去写,在二进制的基础上,衍生出人能看懂的数据类型,进而用看懂的语言来操作数据

二、数据类型

  • 在实际项目开发中要挑选合适的数据类型

1、数据类型简介

  1. Java 中数据类型分为
    • 值类型和引用类型这两大类
  2. 值类型:基本数据类型;
  3. 引用类型:字符串(String)、数组、对象;字符串和数组也是对象;
  4. 引用和值的区别
    • 值类型是实实在在的一个数值;
    • 引用类型是指向内存中的一个地址,修改一个引用可能会带来其他风险;
    • 为了解决风险的问题,很多高级语言中就在函数调用的时候,按值传递,即将引用类型变量拷贝一份作为参数传递,而不是直接传递原来的引用;
    // 值传递
    int a = 10;
    function(a);
    
    void function(int num) {
        // 调用一个函数时,并且该函数接受值类型的参数(比如上面的 int)
        // 那么实际传递给函数的是实参(在这个例子中是变量 a)的值的拷贝。
        num = 11;
        // 因此,函数内部对形参(这里是 num)的任何修改都不会影响到实参 a 的值。
    }
    println(a);  // a还是10,没有变化
    
    
    // 引用传递
    int[] nums = {1,2,3,4,5};
    function(nums);
    void function(int[] nums) {
        nums[0] = 100;
    }
    
    // 因此,要尽量小心地使用引用类型,其实只要尽量少地修改参数即可避免这类问题。
    println(nums[0]);  // nums[0]不是1,是100了
    

2、字符串

  1. 字符串是引用类型,但是编译器对其做了特殊处理,使其使用起来就像值类型,目的是为了不可变
  2. 字符串是代码中使用最多的类型,如果所有的数据类型只能保留一个,那么这个类型一定是字符串(我们一定要像保护我方水晶一样保护我方字符串)
  3. 字符串很重要,我们要保护它的安全
  4. 如果字符串是引用类型,那么就很容易被修改,所以编译器对它进行了特殊处理,使其使用起来就像一个值类型一样
  5. Java 中的 String 就是 final,不但不让你修改,也不让你继承
  6. String 是为了不可变,那么字符串执行自加运算是如何执行的
String a = "abc";
a += "def";  // 结果a为abcdef

这个不是字符串对象变了,而是 a 指向了另一个地址

String a = "abc";  // a执行内存地址001,001放的是"abc"
a += "def";  // a指向的内存地址002,002放的是"abcdef",001放的还是"abc"

即,a 指向了 "abcdef"的内存地址而已,而原来存放 “abc” 的那个地址的值还是 “abc”,并没有变化

也就是说,字符串每次相加都会创建新的字符串对象
而我们知道,创建对象本来就是个高代价操作,所以要尽量避免频繁地执行字符串相加操作,例如下面操作

String name = "";
for (int i = 0; i < names.length; i ++) {
    name += names[i];
}

所以,字符串也不能不加,如果想加,可以使用字符串提供的 API,例如,Java 中的 StringBuilder,StringBuffer;

3、数组

  1. 数组是比较特殊的引用类型,数组指向的是第一个元素在内存中的地址,而且数组是占用一块连续的内存空间

  2. 只需要知道数组的地址(数组的地址也就是数组第一个元素的地址),就能很快定位到第 n 个元素的地址,这也是数组可以根据下标访问元素的原因

  3. 数组必须要有连续的内存空间,如果现在的内存中没有一整块连续的内存空间;

  4. 数组对内存要求苛刻,一旦存满想扩容,就有可能扩容失败

  5. 缺点:内存要求高,扩展性差

4、对象

1)生活场景
  • 碗筷和饭,人吃饭的场景
    • 碗和饭都是对象
    • 其中碗筷属于长生命周期对象,随着我家的存在而存在,随着我家的消失而消失
    • 饭属于短生命周期对象,随出锅而出生,随着被吃下去而消失
    • 所以,任何对象都有它的生命周期
2)函数调用
  1. 局部变量的生命周期,随着函数的调用出生,随着函数调用结束死亡
    • 当一个函数执行到 return 语句时,函数会立即结束执行,并将 return 后面跟随的值(如果有的话)作为函数的返回值

      fun userName() {
          // user 是一个局部变量
          let user = new User();  // 对象出生
          println(user.name);
          return user.name;  // 对象死亡
      }
      
      fun test() {
          let name = UserName();
      }
      
    • 因为,调用一个函数的时候,会将函数的数据读入内存,函数调用完了,也就会把读入的数据清空

    • 上面的例子,因为函数调用完了,引用不存在了,所以 User 型对象没有被引用,会被自动回收掉

    • 所以,每个变量都会定义一个作用域,局部变量的作用域仅限于它所在的函数,一旦函数调用完毕,作用域结束,生命周期走完了,变量就等于死亡了

  2. 还有一种情况,即使函数没有走完,对象也可能死掉
    void test() {
        Object object = new User();  // 创建一个User对象,并用obj这个引用指向它
        // 创建了一个String对象,并用obj这个引用指向它,此时User对象已经没有引用指向了
        object = new String();  
    }
    
    • 当执行完 obj = new String() 后,User 对象已经没有引用指向了,此时进行垃圾回收就可以将 User 对象回收掉,也就是死亡了
    • 上面的例子,虽然函数没有执行完,但是引用指向了另一个对象,所以也导致 User 没有被引用,也会被回收掉,也就是被干掉了
    • 一个对象是否可以被回收,取决于执行垃圾回收时它有没有被引用

3, 将对象置为 null,null 也是一个对象,只不过是一个名字叫做 null 的里面啥都没有的对象

Object null = new Object(); // 创建一个名字为null的空对象
User user = new User();  // 创建一个User对象
user = null;  // 将user指向null,原来的User对象没人指向了,就可以回收了
3)总结

三、代码的工作流程

1、代码执行过程

  • 代码就是用控制语句操作数据类型,进而实现自己的逻辑,然后交给计算机去执行
  • 大致流程
    • 程序员用高级语言写的代码——>编译成机器代码 ——>从外存中读入内存 ——>CPU 执行;
    • 这一系列操作需要需要调动 IO、控制器、运算器、寄存器、程序计数器等元件;

2、计算机元件组成

  • 计算机是由

    • 控制器、
    • 运算器、
    • 存储器、
    • 输入设备:键盘、鼠标
    • 输出设备:显示器
    • 这五部分组成
  • 控制器:是 CPU 中的一部分,用来控制程序的执行流程

  • 程序计数器是控制器的一部分,程序计数器决定着程序的流程;我们的代码被编译成机器码之后,就是从上到下一行一行执行的,具体要执行哪一行,是由程序计数器来进行控制的

  • 运算器:也是 CPU 中的一部分,和控制器共同组成 CPU

    • 运算器的职责就是执行运算
    • 运算器是从寄存器中读取数据,并不是从内存中读取数据
  • 寄存器:也是在 CPU 内部的,是 CPU 中的一部分

    • 通常说的,CPU 从内存中读取数据,就是把数据读入寄存器中;
    • 但是数据并不是直接从内存中读入寄存器中,因为从内存中读取数据太慢,而是先读入高速缓存中,然后才读入寄存器中
    • CPU 先把数据读入高速缓存中,以备使用,真正使用的时候,从高速缓存中读入寄存器
    • 当寄存器使用完毕后,就将数据写回到高速缓存中,然后高速缓存再在合适的时机将数据写入到存储器中
    • CPU 运算速度非常快,而从内存中读取数据非常慢,如果每次都从内存中读取数据,势必会拖累 CPU 的运算速度;为了解决这个问题,就在 CPU 和存储器之间放个高速缓存,CPU 和高速缓存中间的读写速度是非常快的;
    • CPU 只管和高速缓存互换读取数据,而不管高速缓存和内存之间是怎么同步数据的,这就解决了内存读写慢的问题

3、程序的执行流

  • 程序的执行流程分为
    • 顺序执行
    • 条件分支
    • 循环执行

四、面向过程和面向对象

1、面向过程和面向对象的区别

  • 代码执行

    • 人写代码,写完后交给编译器编译为电脑认识的东西,然后电脑将它放到 CPU 中去高速执行,最后将结果展示给我们
      • 分为两次过程
        • 一是我们将写的代码转换为电脑认识的
        • 二是电脑执行完后转换为我们认识的
        • 即,我们写的代码是面向我们编写人员的,编译为电脑执行的代码是面向计算机的
  • 面向过程

    • 即,是以当前发生的事情为目标进行编程,以过程为核心,不考虑将来可能发生的事情(只注重当前,不注重未来的扩展性)
  • 面向对象

    • 将世间万物都视为对象,针对这些对象的具体行为进行编码
    • 三大特点:封装、继承、多态

2、面向对象

1)封装

封装就是将现实事物封装成抽象的类,并可以将类的权限加以修改,把自己的数据和方法只让可信的类或者对象操作,对不可信的信息隐藏
即你只能看我给你看的,只能修改我让你改的

  • 封装是类和对象概念的主要特性,封装让对象针对不同的角色有不同的表现,让程序更有层次感
  • 如果没有封装,那么继承就没有意义,多态也就不存在
  • 封装的作用
    • 修改权限
    • 将具体事物抽象成类的这个过程;例如,将现实中的一辆汽车,把它抽象成一个类:class Car { }
2)继承

让类拥有另一个类的数据和函数,表达一种我也是你的关系

  • 继承的作用
    • 继承可以节省大量代码,并且容易扩展
    • 子类的公有属性和行为都可以放在父类中,而子类只需要放自己特有的属性和行为即可,这样可以统一处理子类的共有属性,方便管理,子类的特有属性又可以在内部自己处理
    • 继承就是为了多态
      • 假如,我有奥迪、宝马、奔驰、大众,我可以直接抽象出公共部分定义一个父类 Car(属性为 price,方法为 run),然后创建出各个车的类直接继承 Car 即可,就不用反复声明 price 和 run 了
      • 但是,每个车的价格和速度又不一样,直接继承是有问题的,这就涉及到多态
      • 即,继承就是为了多态
3)多态

多态指的是一个类的子类可以有不同的表现

  • 多态的作用
    • 为了让代码容易修改,容易扩展;
    • 没有多态,就没有设计模式
4)接口
  • 继承关系只能有一个,即只能有一个父类,那需要多继承的时候怎么办?通过接口
  • 接口表示我具有某种功能,继承表示我就是某种东西;因为你只能是一种东西,所以只能单继承,但是可以具有多个功能,实现多个接口
    interface CanFly {}
    interface CanSwim {}
    interface CanChange {}
    
    class NvWa {}
    class SunWuKong extends NvWa implements CanFly, CanSwim, CanChange {}  
    

五、进程和线程

1、软件简介

  • 什么是软件
    • 软件就是程序的载体。例如,我们写的 hello world 等代码,打包出来就是软件,可以安装在各种设备上,然后运行在操作系统上
  • 软件分为
    • 系统软件
    • 应用软件
    • 操作系统就是系统软件的一种,是最基础的软件,其余所有软件都要运行在操作系统上

2、操作系统简介

  • 操作系统是软件,是专门用来管理软件的软件
  • 作用
    • 负责和硬件打交道,将用户的各种操作翻译给计算机硬件,然后将结果告诉用户
    • 管理其他软件

3、进程和线程

  • 操作系统是如何管理软件的

    • 通过管理进程
    • 软件是程序的载体,每个程序都有一个进程
  • 每个程序都有一个进程,线程是进程的一部分,线程是操作系统能调度的最小单位

  • 使用线程可以提高程序的运行速度,节省时间和成本

  • 资源都是在进程中的,进程是持有资源的最小单位

  • 线程是进程的一部分,是操作系统调度的最小单位,是调度的最小单位,不是持有资源的最小单位,线程可以使用资源,但是不能持有资源

  • 每个进程都有一块内存,这个内存是所有线程所共享的

  • 每个线程都有一个缓存,这个缓存是自己独占的

4、多线程

  • 资源冲突问题

    • 多个线程在同一时刻操作同一个资源;
    • 多个访问资源不会产生冲突,操作资源会产生冲突(操作资源就是对资源进行修改)
  • 解决资源冲突问题

  • 不同时、不同目标

  • 不同时:排队!将并行变为串行,可以在 i 被读入线程 A 的内存中时,就加个标记,其它线程看到这个标记就会等待,等线程 A 执行完将 i 写回到内存中时,再把标记清除,并通知其它线程,其它线程再开始执行

  • 并发即解决方案

    • 加锁

      • 在变量 a 被一个线程读取后,就加个锁,其它线程过来之后发现有锁,就等着,就会导致同时只能有一个线程使用 a
    • 如果被写回来,就释放锁

    • 通过线程获取锁的方式,实现了将并行转换为串行

      class A {
          int a = 0;
      
          Thread t1 = new Thread() {
              synchronized void run() {
                  for (int i = 0; i < 1000; i ++)
                      a ++;
              
              }
          };
          t1.start();
      
          Thread t2 = new Thread() {
              synchronized void run() {
                  for (int i = 0; i < 1000; i ++)
                      a ++;
              }
          };
          t2.start();
      }
      

5、乐观锁和悲观锁

1)乐观处理和悲观处理
  • 并发的处理免不了要等待操作
  • 分为两种情况
    • 我等着,你好了叫我
    • 我等着,每隔一段时间再来看看你搞完了么
  • 悲观处理:你好了叫我,可能需要等好久,所以就是假设要等很长时间,这是带有悲观色彩的,就称为悲观处理
  • 乐观处理:我等一会再来看看,明显就是可能你很快就完了,也就是只等很短时间,这是带乐观色彩的,就称为乐观处理
2)乐观锁和悲观锁
  • 悲观锁

    • 悲观锁的核心就是等待,被唤醒
    • 即我可能要等好久,那就算了,我不等了,你完事后跟我说
    • 以此我们发明的悲观锁就叫 Synchronized
  • 乐观锁

    • 乐观锁的核心是自旋

    • CAS 就是个乐观锁

    • 乐观锁因为只要能很少的时间,所以,与其我回家等着,不如我就在这看着,反正也不会等很久,过一段时间就来问问

    • 这种过一段时间就来看看的叫做轮询,其中过一段时间过来问问的叫做轮询,这段时间在干啥呢,是在原地转圈玩,反正不闲着,这就叫做自旋

    • 因此,可以发明一种自旋锁,自旋锁就是乐观锁的一种

      class A {
      	int a = 0;
      	boolean lock = false;
      	
      	// 开启一个线程
      	Thread t1 = new Thread() {
      		// 加了synchronized关键字
      		void run() {
      			// 运行时发现被锁了,就自己死循环
      			while (lock) {  // 在 while(true)中让线程休眠一段时间,如果好几次都获取不到锁,那就增加休眠时间
      				int len = 0;
      				while (true) {
      					sleep((length ++) * 200);
      				}
      			}
      			// 终于拿到锁了,执行自己的逻辑
      			lock = true;  // 自己拿到锁了,标记一下
      			// 操作资源
      			for (int i = 0; i < 10000000; i ++) a ++;
      			lock = false;  // 自己执行完了,释放锁
      		}
      	};
      	t1.start();
      
      	// 开启另一个线程
      	Thread t2 = new Thread() {
      		// 加了synchronized关键字
      		void run() {
      			// 运行时发现被锁了,就自己休眠
      			while (lock) {
      				int len = 0;
      				while (true) {
      					sleep((length ++) * 200);
      				}
      			}
      			// 终于拿到锁了,执行自己的逻辑
      			lock = true;  // 自己拿到锁了,标记一下
      			// 操作资源
      			for (int i = 0; i < 10000000; i ++) a ++;
      			lock = false;  // 自己执行完了,释放锁
      		}
      	};
      	t2.start();
      }
      
  • 很多语言中提供了现成的乐观锁的工具 API,有 CAS 这个关键字

  • 总结

    • 凡是要等很久的都是悲观处理,以此可以发明一个悲观锁
    • 凡是可能等很少时间的都是乐观处理,以此可以发明一个乐观锁
    • 根据具体的场合和场景去选用适合的锁
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一个写代码的修车工

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值