面试题- Java - 基础部分

220 阅读16分钟

1. 抽象类与接口的区别?

作为面试者,我会这样回答: "抽象类和接口是Java中两种不同的抽象机制,主要区别如下:

  1. 实现方式:
  • 类只能继承一个抽象类(extends)
  • 但可以实现多个接口(implements)
  1. 方法特性:
  • 抽象类可以包含普通方法和抽象方法
  • 接口中在Java 8之前只能有抽象方法,Java 8后可以有默认方法(default)和静态方法
  1. 成员变量:
  • 抽象类可以包含各种访问级别的成员变量
  • 接口中的变量默认是 public static final 的

举个实际例子:"

// 抽象类示例
abstract class Animal {
    protected String name;  // 可以有成员变量
    public abstract void makeSound();  // 抽象方法
    public void eat() {  // 普通方法
        System.out.println("动物在进食");
    }
}

// 接口示例
interface Flyable {
    int MAX_HEIGHT = 10000;  // 默认public static final
    void fly();  // 默认public abstract
}

2. final、static 和 synchronized 的修饰作用?

"这三个关键字在Java中都很重要,让我详细说明:

  1. final 可以修饰:
  • 类:表示类不能被继承
  • 方法:表示方法不能被重写
  • 变量:表示变量是常量,只能赋值一次
  1. static 可以修饰:
  • 变量:成为类变量,被所有实例共享
  • 方法:成为类方法,不需要实例就能调用
  • 代码块:类加载时执行
  • 内部类:可以不依赖外部类实例而存在
  1. synchronized 可以修饰:
  • 方法:整个方法成为同步方法
  • 代码块:特定代码块成为同步区域

示例代码:"

public class Example {
    // final示例
    final class FinalClass { }  // 不能被继承
    final int constant = 100;   // 不能被修改
    
    // static示例
    static int classVar = 0;    // 类变量
    static {                    // 静态代码块
        System.out.println("类初始化");
    }
    
    // synchronized示例
    synchronized void method() { // 同步方法
        // 线程安全的代码
    }
}

3. String、StringBuffer 和 StringBuilder 的区别?

"这三个类的主要区别在于可变性和线程安全性:

  1. String:
  • 不可变的字符序列
  • 线程安全
  • 每次操作都会创建新对象
  1. StringBuffer:
  • 可变的字符序列
  • 线程安全(方法都是synchronized的)
  • 性能比StringBuilder稍差
  1. StringBuilder:
  • 可变的字符序列
  • 非线程安全
  • 性能最好

使用建议:

  • 如果字符串不经常变化,用String
  • 多线程环境下,用StringBuffer
  • 单线程环境下,用StringBuilder

示例:"

String str = "Hello";  // 不可变
str += " World";  // 创建新对象

StringBuffer sbf = new StringBuffer("Hello");
sbf.append(" World");  // 同一对象修改,线程安全

StringBuilder sbd = new StringBuilder("Hello");
sbd.append(" World");  // 同一对象修改,非线程安全

好的,我继续回答剩余的问题:

4. "equals"与"=="、"hashCode"的区别和使用场景?

"这是一个经典问题,让我详细解释:

  1. == 运算符:
  • 比较基本数据类型时,比较值
  • 比较引用类型时,比较内存地址
  1. equals方法:
  • Object类的默认实现等同于==
  • 常被重写用于比较对象的内容
  • String等类已重写equals实现内容比较
  1. hashCode:
  • 返回对象的哈希码值
  • 相等的对象必须有相同的hashCode
  • 常用于HashMap等集合类中

示例代码:"

public class EqualsExample {
    public static void main(String[] args) {
        String str1 = new String("hello");
        String str2 = new String("hello");
        
        System.out.println(str1 == str2);        // false(比较地址)
        System.out.println(str1.equals(str2));   // true(比较内容)
        System.out.println(str1.hashCode() == str2.hashCode()); // true
    }
}

5. Java中深拷贝与浅拷贝的区别?

"深浅拷贝的区别主要在于对象内部的引用类型成员的处理方式:

  1. 浅拷贝:
  • 复制对象的基本数据类型值
  • 复制对象的引用但不复制引用的对象
  • 实现Cloneable接口的默认clone()方法是浅拷贝
  1. 深拷贝:
  • 复制对象的基本数据类型值
  • 复制引用对象并重新创建
  • 可通过重写clone()方法实现

示例代码:"

public class CopyExample implements Cloneable {
    private int[] data;
    
    // 浅拷贝
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    
    // 深拷贝
    public CopyExample deepClone() throws CloneNotSupportedException {
        CopyExample result = (CopyExample) super.clone();
        result.data = this.data.clone();
        return result;
    }
}

6. Error和Exception的区别?

"这两者都是Throwable的子类,但有重要区别:

  1. Error:
  • 表示严重的问题,通常是不可恢复的
  • 程序通常不应该捕获Error
  • 例如:OutOfMemoryError、StackOverflowError
  1. Exception:
  • 表示可处理的问题
  • 分为受检异常(Checked)和非受检异常(Unchecked)
  • 受检异常必须处理(try-catch或throws)
  • RuntimeException及其子类是非受检异常

示例:"

public class ExceptionExample {
    public void method() throws IOException {  // 受检异常必须声明
        try {
            // 可能抛出异常的代码
            throw new IOException();
        } catch (IOException e) {
            // 处理异常
        }
        
        // RuntimeException可以不声明
        throw new RuntimeException();
    }
}

7. 什么是反射机制?反射机制的应用场景?

"反射是Java的一个重要特性:

  1. 反射的概念:
  • 在运行时检查和修改类的结构和行为
  • 可以访问私有成员
  • 可以动态创建对象和调用方法
  1. 主要应用场景:
  • 框架开发(Spring、Hibernate等)
  • 注解的处理
  • 动态代理
  • 泛型擦除的补偿

示例代码:"

public class ReflectionExample {
    public static void main(String[] args) throws Exception {
        // 获取类信息
        Class<?> clazz = Class.forName("java.lang.String");
        
        // 创建实例
        Object obj = clazz.newInstance();
        
        // 获取方法
        Method method = clazz.getMethod("length");
        
        // 调用方法
        method.invoke(obj);
    }
}

8. 如何重写equals()方法?为什么要重写hashCode()?

"这是一个很重要的问题:

  1. equals方法重写原则:
  • 自反性:x.equals(x)返回true
  • 对称性:x.equals(y)和y.equals(x)结果相同
  • 传递性:x.equals(y)和y.equals(z)都为true,则x.equals(z)也为true
  • 一致性:多次调用结果相同
  • 非空性:x.equals(null)返回false
  1. 为什么要重写hashCode:
  • equals相等的对象必须有相同的hashCode
  • 用于HashMap等集合类的正确工作
  • 提高散列集合的性能

示例代码:"

public class Person {
    private String name;
    private int 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);
    }
}

继续回答剩余问题:

9. Java中IO流分为几种?它们之间有什么区别?

"Java的IO流主要分为以下几类:

  1. 按照数据流向:
  • 输入流(InputStream、Reader)
  • 输出流(OutputStream、Writer)
  1. 按照数据类型:
  • 字节流(InputStream、OutputStream)
  • 字符流(Reader、Writer)
  1. 常用流的分类:
  • 缓冲流:BufferedInputStream、BufferedReader等
  • 文件流:FileInputStream、FileWriter等
  • 对象流:ObjectInputStream、ObjectOutputStream

示例代码:"

public class IOExample {
    public void ioDemo() throws IOException {
        // 字节流示例
        try (FileInputStream fis = new FileInputStream("file.txt");
             BufferedInputStream bis = new BufferedInputStream(fis)) {
            int data = bis.read();
        }
        
        // 字符流示例
        try (FileReader fr = new FileReader("file.txt");
             BufferedReader br = new BufferedReader(fr)) {
            String line = br.readLine();
        }
    }
}

10. Java泛型中类型擦除的理解及局限性?

"泛型是Java 5引入的重要特性:

  1. 类型擦除的概念:
  • 编译器在编译时擦除泛型信息
  • 将泛型类型替换为原始类型(通常是Object)
  • 插入必要的类型转换
  1. 主要局限性:
  • 不能获取泛型类型的实际类型参数
  • 不能创建泛型数组
  • 不能用基本类型作为类型参数
  • 静态成员不能引用类型参数

示例代码:"

public class GenericExample<T> {
    // 不能创建泛型数组
    // T[] array = new T[10]; // 编译错误
    
    // 不能在静态上下文中使用T
    // static T staticField; // 编译错误
    
    public void method(T t) {
        // 运行时无法获知T的实际类型
        if (t instanceof T) {} // 编译错误
    }
}

11. String为什么要设计成不可变的?

"String的不可变性有很多重要原因:

  1. 安全性:
  • 用作HashMap的key时更安全
  • 在多线程环境下无需同步
  1. 性能优化:
  • 字符串常量池的实现
  • 缓存hashCode值
  1. 其他优势:
  • 线程安全
  • 支持String对象共享

示例说明:"

public class StringExample {
    public void demo() {
        String str1 = "hello";  // 进入常量池
        String str2 = "hello";  // 复用常量池中的对象
        System.out.println(str1 == str2);  // true
        
        // 即使修改也是创建新对象
        str1 += "world";  // 创建新的String对象
    }
}

12. Java注解的理解?

"注解是Java 5引入的特性:

  1. 基本概念:
  • 提供程序元数据的方式
  • 不直接影响代码执行
  • 可以被编译器和运行时处理
  1. 常见用途:
  • 编译检查(@Override)
  • 代码生成(Lombok)
  • 运行时处理(Spring的@Autowired)

示例代码:"

// 自定义注解示例
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAnnotation {
    String value() default "";
}

// 使用注解
public class AnnotationExample {
    @MyAnnotation("测试")
    public void testMethod() {
        // 方法实现
    }
}

13. Java成员变量、局部变量和静态变量的创建和回收时机?

"不同类型变量的生命周期不同:

  1. 成员变量:
  • 创建:对象实例化时创建
  • 回收:对象被垃圾回收时回收
  1. 局部变量:
  • 创建:方法调用时在栈上创建
  • 回收:方法结束时回收
  1. 静态变量:
  • 创建:类加载时创建
  • 回收:类卸载时回收

示例代码:"

public class VariableExample {
    private int instanceVar;     // 成员变量
    private static int staticVar; // 静态变量
    
    public void method() {
        int localVar = 0;        // 局部变量
        // 方法结束时localVar被回收
    }
}

14. Java中String.length()的运作原理?

"String.length()的实现相对简单:

  1. 基本原理:
  • String内部使用char[]数组存储字符
  • length()返回这个数组的长度
  • Java 9之后改用byte[]加编码标记存储
  1. 特点:
  • O(1)时间复杂度
  • 返回字符数而非字节数
  • 统计的是Unicode代码单元的数量

示例说明:"

public class StringLengthExample {
    public void demo() {
        String str = "Hello";
        // length()直接返回内部字符数组的长度
        int len = str.length();  // 返回5
        
        // 注意:对于某些Unicode字符
        String emoji = "👨‍👩‍👧‍👦";
        System.out.println(emoji.length());  // 可能大于1
    }
}

15. 注解应用的详解

1. 注解定义

@Retention(RetentionPolicy.RUNTIME)  // 1
@Target(ElementType.METHOD)          // 2
public @interface LogExecutionTime {
    String description() default "";  // 3
}

代码解释:

  1. @Retention(RetentionPolicy.RUNTIME)

    • 表示这个注解在运行时可以通过反射访问
    • 其他选项包括:SOURCE(编译时丢弃)和CLASS(类加载时丢弃)
  2. @Target(ElementType.METHOD)

    • 指定注解只能用在方法上
    • 其他常见选项:TYPE(类)、FIELD(字段)、PARAMETER(参数)等
  3. description()

    • 注解的属性,可以在使用注解时指定描述文本
    • default ""表示默认值为空字符串

2. 注解处理器(AOP实现)

@Aspect                 // 1
@Component              // 2
public class LogAspect {
    private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);  // 3

    @Around("@annotation(logExecutionTime)")    // 4
    public Object logExecutionTime(ProceedingJoinPoint joinPoint, LogExecutionTime logExecutionTime) throws Throwable {
        // 记录开始时间
        long startTime = System.currentTimeMillis();  // 5
        
        // 执行原方法
        Object result = joinPoint.proceed();         // 6
        
        // 计算执行时间
        long endTime = System.currentTimeMillis();
        long duration = endTime - startTime;         // 7
        
        // 获取方法信息
        String methodName = joinPoint.getSignature().getName();  // 8
        String description = logExecutionTime.description();     // 9
        
        // 记录日志
        logger.info("方法 [{}] {} 执行耗时: {}ms", 
            methodName, 
            description, 
            duration);                               // 10
            
        return result;                              // 11
    }
}

代码解释:

  1. @Aspect:声明这是一个切面类,用于实现AOP
  2. @Component:将该类注册为Spring容器管理的Bean
  3. 创建日志记录器实例
  4. @Around:环绕通知,可以在目标方法执行前后添加逻辑
  5. 记录方法执行开始时间
  6. joinPoint.proceed():执行原始方法
  7. 计算方法执行耗时
  8. 获取被执行方法的名称
  9. 获取注解中的描述信息
  10. 使用日志记录执行信息
  11. 返回原方法的执行结果

3. 实际使用示例

@Service                // 1
public class UserService {
    
    @LogExecutionTime(description = "查询用户信息")  // 2
    public User getUserById(Long userId) {
        // 模拟数据库查询
        try {
            Thread.sleep(1000);    // 3
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return new User(userId, "测试用户");
    }
    
    @LogExecutionTime(description = "创建新用户")   // 4
    public User createUser(UserDTO userDTO) {
        // 创建用户的业务逻辑
        return new User(userDTO.getId(), userDTO.getName());
    }
}

代码解释:

  1. @Service:标记这是一个服务类,由Spring容器管理
  2. 在方法上使用自定义注解,并提供描述信息
  3. 模拟耗时操作
  4. 在另一个方法上使用相同的注解

4. 工作流程

  1. 当调用被 @LogExecutionTime 注解标记的方法时:
userService.getUserById(1L);
  1. Spring AOP会拦截这个调用,执行顺序如下:

    • 进入LogAspect的logExecutionTime方法
    • 记录开始时间
    • 执行原始方法(getUserById)
    • 计算执行时间
    • 记录日志
    • 返回结果
  2. 最终在日志中看到:

方法 [getUserById] 查询用户信息 执行耗时: 1000ms

实际应用扩展

这个模式可以扩展用于多种场景:

// 性能监控
@Monitor(threshold = 1000)
public void performanceMethod() { }

// 接口限流
@RateLimit(value = 100, timeUnit = TimeUnit.SECONDS)
public void rateLimitMethod() { }

// 缓存处理
@Cacheable(key = "#userId")
public User getUser(Long userId) { }

这种注解驱动的开发方式使代码更加清晰、可维护,并且便于统一管理横切关注点(如日志、性能监控、安全等)。

16. string问题的延伸

让我们通过一个生动的例子来理解String的不可变性:

1. String在内存中的存储

String str1 = "Hello";
String str2 = "Hello";
String str3 = new String("Hello");

想象成这样的场景:

  1. 字符串常量池就像一个特殊的图书馆
  2. 普通的堆内存就像是个人书房
  3. String对象就像是一本书

让我用图来展示:

堆内存(书房)
┌──────────────────────────────────┐
│                                  │
│     String对象 str3 ──────┐      │
│                          ↓      │
│                       "Hello"    │
│                                  │
└──────────────────────────────────┘

字符串常量池(图书馆)
┌──────────────────────────────────┐
│                                  │
│           "Hello"  ←─── str1     │
│              ↑                   │
│              └──── str2          │
│                                  │
└──────────────────────────────────┘

2. 为什么要设计成不可变?

2.1 安全性考虑
HashMap<String, User> userMap = new HashMap<>();
String key = "userKey";
userMap.put(key, new User());

// 如果String是可变的:
key.change("newKey");  // 假设可以修改
// 这时候还能找到原来的User对象吗?

就像这样:

  • 把String作为钥匙🔑
  • HashMap就像保险箱🏦
  • 如果钥匙可以随意变形,我们就找不到放进去的东西了!
2.2 线程安全
// 在多线程环境下:
public class SharedString {
    private String name = "initial"; // 不可变,所以安全
    
    public void process() {
        // 多个线程可以同时读取name
        // 即使修改也是创建新对象,不会影响其他线程
    }
}

想象成:

  • String就像是一个密封的信封📨
  • 多个人(线程)可以同时读
  • 要修改时会创建新信封,而不是修改原信封
2.3 性能优化(字符串常量池)
String s1 = "hello";
String s2 = "hello";
String s3 = "hel" + "lo";  // 编译时优化

这就像:

字符串常量池(图书馆)
┌────────────────────────┐
│                        │
│   "hello" ←─── s1      │
│      ↑                 │
│      ├──── s2         │
│      └──── s3         │
│                        │
└────────────────────────┘
  • 常量池就像图书馆的共享区
  • 相同的字符串只存一份
  • 节省内存,提高性能

3. 实际操作中的内存变化

String str = "Hello";
str = str + " World";

内存中的变化过程:

步骤1:
常量池:["Hello"]  ←─── str

步骤2:
堆内存:创建新的 "Hello World"
常量池:["Hello"]
       ["Hello World"] ←─── str

原来的"Hello"对象等待GC回收

4. String的内部实现

public final class String {
    private final byte[] value;  // Java 9之后使用byte[]
    private final byte coder;    // 编码标记
    
    // value数组一旦创建就不能修改
    // 所有的修改操作都会创建新对象
}

就像:

  • String是一个封闭的盒子📦
  • 里面的内容一旦放进去就封死了
  • 要改变就只能重新用一个新盒子

5. 性能考虑

// 不推荐
String result = "";
for (int i = 0; i < 1000; i++) {
    result += i;  // 每次都创建新对象
}

// 推荐
StringBuilder result = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    result.append(i);  // 在同一对象上修改
}

这就像:

  • String操作就像每次都要新买一个盒子
  • StringBuilder就像一个可以反复使用的容器
  • 当需要大量修改时,用StringBuilder更经济

通过这种方式,我们可以看到String的不可变性不仅仅是一个设计选择,而是为了:

  1. 保证数据的安全性(特别是在HashMap等集合中)
  2. 实现字符串常量池的优化
  3. 确保多线程环境下的安全性
  4. 支持String对象的共享使用

这种设计虽然可能在某些场景下看起来不够灵活,但总体上带来的好处远大于局限性。

17. 涉及到的内存模型

让我详细讲解Java内存模型(JMM - Java Memory Model):

1. Java内存模型的基本结构

让我重新绘制一个更清晰的Java内存模型图:

┌──────────────────────────── JMM (Java Memory Model) ────────────────────────────┐
│                                                                                 │
│    Thread-1                   Main Memory                      Thread-2         │
│  ┌──────────┐             ┌──────────────┐                  ┌──────────┐       │
│  │          │             │              │                  │          │       │
│  │ Working  │ ◄─────────► │  Shared      │ ◄─────────────► │ Working  │       │
│  │ Memory   │             │  Variables   │                  │ Memory   │       │
│  │          │             │              │                  │          │       │
│  └──────────┘             └──────────────┘                  └──────────┘       │
│       ▲                                                          ▲             │
│       │                                                          │             │
│       │                                                          │             │
│       ▼                                                          ▼             │
│  ┌──────────┐                                              ┌──────────┐       │
│  │   CPU    │                                              │   CPU    │       │
│  │  Cache   │                                              │  Cache   │       │
│  └──────────┘                                              └──────────┘       │
│                                                                                 │
└─────────────────────────────────────────────────────────────────────────────────┘

这个图展示了Java内存模型的核心组件:

  1. 主内存(Main Memory)

    • 存储所有共享变量的主副本
    • 所有线程都可以访问
  2. 工作内存(Working Memory)

    • 每个线程都有自己的工作内存
    • 存储主内存变量的副本
    • 线程对变量的操作都在工作内存中进行
  3. CPU缓存(CPU Cache)

    • 位于主内存和工作内存之间
    • 用于提高数据访问速度
    • 可能导致可见性问题
  4. 数据流动

    • 线程不能直接访问主内存
    • 必须将变量从主内存拷贝到工作内存
    • 修改后再将结果写回主内存

这种架构设计是导致并发问题的根本原因,也是需要使用同步机制的原因。

2. 内存模型的核心概念

public class MemoryModelExample {
    private int x = 0;    // 共享变量
    private boolean flag = false;  // 共享变量
    
    // 线程1执行
    public void writer() {
        x = 42;           // 1
        flag = true;      // 2
    }
    
    // 线程2执行
    public void reader() {
        if (flag) {       // 3
            System.out.println(x);  // 4
        }
    }
}

这段代码可能出现的问题:

  1. 重排序:1和2的执行顺序可能互换
  2. 可见性:线程2可能看不到线程1的修改
  3. 原子性:操作可能被打断

3. JMM的三大特性

3.1 原子性(Atomicity)

public class AtomicityExample {
    private volatile int count = 0;  // volatile不保证原子性
    
    // 非原子操作
    public void increment() {
        count++;  // 实际上是三个操作:读取、增加、写入
    }
    
    // 使用原子类保证原子性
    private AtomicInteger atomicCount = new AtomicInteger(0);
    public void safeIncrement() {
        atomicCount.incrementAndGet();  // 原子操作
    }
}

3.2 可见性(Visibility)

public class VisibilityExample {
    // volatile保证可见性
    private volatile boolean flag = false;
    private int number = 0;
    
    public void writer() {
        number = 42;    // 1
        flag = true;    // 2: volatile写,保证1的结果对其他线程可见
    }
    
    public void reader() {
        if (flag) {     // volatile读
            System.out.println(number);  // 一定能看到42
        }
    }
}

3.3 有序性(Ordering)

public class OrderingExample {
    private int a = 0;
    private boolean flag = false;
    
    public void write() {
        a = 1;                // 1
        flag = true;          // 2
    }
    
    // 使用synchronized保证有序性
    public synchronized void read() {
        if (flag) {           // 3
            System.out.println(a);  // 4
        }
    }
}

4. 内存屏障(Memory Barrier)

public class MemoryBarrierExample {
    private volatile int value = 0;
    
    public void setValue(int value) {
        // 写入volatile变量前的写操作不会被重排序到写入volatile之后
        this.value = value;    // 写屏障
    }
    
    public int getValue() {
        // 读取volatile变量后的读操作不会被重排序到读取volatile之前
        return value;          // 读屏障
    }
}

5. Happens-Before规则

public class HappenBeforeExample {
    private volatile int value = 0;
    
    public void example() {
        // 1. 程序顺序规则
        int a = 1;
        int b = 2;  // 一定在a=1之后执行
        
        // 2. volatile变量规则
        value = 3;  // 之前的所有写操作对后续的读操作可见
        
        // 3. 传递性规则
        // 如果A happens-before B,B happens-before C
        // 那么A happens-before C
    }
}

6. 实际应用示例

public class PracticalExample {
    // 双重检查锁定的单例模式
    private static volatile PracticalExample instance;
    
    public static PracticalExample getInstance() {
        if (instance == null) {  // 第一次检查
            synchronized (PracticalExample.class) {
                if (instance == null) {  // 第二次检查
                    instance = new PracticalExample();
                }
            }
        }
        return instance;
    }
}

7. 常见的同步工具

public class SynchronizationTools {
    // 1. synchronized关键字
    private synchronized void syncMethod() {
        // 同步方法
    }
    
    // 2. ReentrantLock
    private ReentrantLock lock = new ReentrantLock();
    public void lockMethod() {
        lock.lock();
        try {
            // 临界区代码
        } finally {
            lock.unlock();
        }
    }
    
    // 3. volatile变量
    private volatile boolean flag;
    
    // 4. 原子类
    private AtomicInteger atomicInt = new AtomicInteger(0);
}

理解Java内存模型对于编写正确的并发程序至关重要。它帮助我们:

  1. 理解并发问题的本质
  2. 正确使用同步机制
  3. 避免常见的并发陷阱
  4. 提高程序的性能和可靠性

记住:

  • volatile适合一写多读的场景
  • synchronized适合临界区操作
  • Atomic类适合原子操作需求
  • 正确理解happens-before规则有助于写出正确的并发代码