热门标签 | HotTags
当前位置:  开发笔记 > 运维 > 正文

举例讲解Java的RTTI运行时类型识别机制

这篇文章主要介绍了Java的RTTI运行时类型识别机制,包括泛化的Class引用以及类型检查instanceof等知识点,需要的朋友可以参考下

1、RTTI:
运行时类型信息可以让你在程序运行时发现和使用类型信息。

在Java中运行时识别对象和类的信息有两种方式:传统的RTTI,以及反射。下面就来说下RTTI。

RTTI:在运行时,识别一个对象的类型。但是这个类型在编译时必须已知。

下面通过一个例子来看下RTTI的使用。这里涉及到了多态的概念:让代码只操作基类的引用,而实际上调用具体的子类的方法,通常会创建一个具体的对象(Circle,Square,或者Triangle,见下例),把它向上转型为Shape(忽略了对象的具体类型),并在后面的程序中使用匿名(即不知道具体类型)的Shape引用:

20165985229239.png (515×293)

abstract class Shape {
  // this 调用当前类的toString()方法,返回实际的内容
  void draw(){ System.out.println(this + "draw()"); }
  // 声明 toString()为abstract类型,强制集成在重写该方法
  abstract public String toString();
}

class Circle extends Shape {
  public String toString(){ return "Circle"; }
}

class Square extends Shape {
  public String toString(){ return "Square"; }
}

class Triangle extends Shape {
  public String toString(){ return "Triangle"; }
}

public static void main(String[] args){
  // 把Shape对象放入List的数组的时候会向上转型为Shape,从而丢失了具体的类型信息
  List shapeList = Arrays.asList(new Circle(), new Square(), new Triangle());
  // 从数组中取出时,这种容器,实际上所有的元素都当成Object持有,会自动将结果转型为Shape,这就是RTTI的基本的使用。
  for(Shape shape : shapeList){
    shape.draw();
  }
}

输出结果为:

Circledraw()
Squaredraw()
Triangledraw()

存入数组的时候,会自动向上转型为Shape,丢失了具体的类型,当从数组中取出的时候,(List容器将所有的事物都当做Object持有),会自动将结果转型回Shape,这就是RTTI的基本用法。Java中所有的类型转换都是在运行时进行正确性检查的,也就是RTTI:在运行时,识别一个对象的类型。

上面的转型并不彻底,数组的元素取出时又Object转型为Shape,而不是具体的类型,编译时这是由容器和Java泛型系统来确保这一点的,而在运行时时有类型转换操作来确保这一点的。

而能够通过Shape对象执行到子类的具体代码就是又多态来决定的了,具体看Shape引用所指向的具体对象。

另外,使用RTTI,可以查询某个Shape引用所指向的对象的确切类型,然后选择性的执行子类的方法。

2、Class对象:
要了解RTTI在Java中的工作原理,必须知道类型信息在运行时是如何表示的,这里是由Class这个特殊对象完成的。

Class对象是用来创建类的所有的“常规”对象的。Java使用Class对象来执行其RTTI。

每当编译一个新类,就会产生一个Class对象(.class文件)。运行这个程序的JVM将使用“类加载器”这个子系统。

类加载器子系统:包含一条类加载器链,但只有一个原生类加载器,它是JVM实现的一部分。原生类加载器加载可信类,包括Java API类,通常是从本地磁盘加载的。当需要以某种特定的方式加载类,以支持Web服务器应用,可以挂接额外的类加载器。

2.1、加载类的时机:
当程序创建第一个对类的静态成员的引用时,就会加载这个类。这证明其实构造器也是类的静态方法,当使用new操作符创建类的新对象也会当做对类的静态成员的引用。

可见Java程序时动态加载的,按需加载。需要用到Class时,类加载器首先会检查这个类的Class对象是否已经加载,如果尚未加载,默认的类加载器就会根据类名查找到.class文件。接下来是验证阶段:加载时,它们会接受验证,以确保其没有被破坏,并且不包含不良Java代码。

2.2、Class相关方法,newInstance()
下面通过一个例子演示Class对象的加载:

class A {
  // 静态代码库,在第一次被加载时执行,通过打印信息知道该类什么时候被加载
  static { System.out.println("Loading A"); }
}
class B {
  static { System.out.println("Loading B"); }
}
class C {
  static { System.out.println("Loading C"); }
}
public class Load {
  public static void main(String[] args){
    System.out.println("execute main...");
    new A();
    System.out.println("after new A");
    try {
      Class.forName("com.itzhai.test.type.B");
    } catch (ClassNotFoundException e) {
      System.out.println("cloud not find class B");
    }
    System.out.println("after Class.forName B");
    new C();
    System.out.println("after new C");
  }
}

输出结果为:

execute main...
Loading A
after new A
Loading B
after Class.forName B
Loading C
after new C

可见,Class对象在需要的时候才被加载,注意到这里的Class.forName()方法:

forName()方法是取得Class对象的引用的一种方法,通过这个方法获得恰当的Class对象的引用,就可以在运行时使用类型信息了。

如果你已经有了一个感兴趣的类型的对象,则可以通过跟类Object提供的getClass()方法来获得Class引用。

下面是一段Class的使用的代码:

interface X{}
interface Y{}
interface Z{}
class Letter {
  Letter(){};
  Letter(int i){};
}
class NewLetter extends Letter implements X, Y, Z{
  NewLetter(){ super(1); };
}
public class ClassTest {

  /**
   * 打印类型信息
   * @param c
   */
  static void printInfo(Class c){
    // getName()获得全限定的类名
    System.out.println("Class name: " + c.getName() + " is interface? " + c.isInterface());
    // 获得不包含包名的类名
    System.out.println("Simple name: " + c.getSimpleName());
    // 获得全限定类名
    System.out.println("Canonical name: " + c.getCanonicalName());
  }

  public static void main(String[] args){
    Class c = null;
    try {
      // 获得Class引用
      c = Class.forName("com.itzhai.test.type.NewLetter");
    } catch (ClassNotFoundException e) {
      System.out.println("Can not find com.itzhai.test.type.NewLetter");
      System.exit(1);
    }
    // 打印接口类型信息
    for(Class face : c.getInterfaces()){
      printInfo(face);
    }
    // 获取超类Class引用
    Class up = c.getSuperclass();
    Object obj = null;
    try {
      // 通过newInstance()方法创建Class的实例
      obj = up.newInstance();
    } catch (InstantiationException e) {
      System.out.println("Can not instantiate");
    } catch (IllegalAccessException e) {
      System.out.println("Can not access");
    }
    // 打印超类类型信息
    printInfo(obj.getClass());
  }
}

输出为:

Class name: com.itzhai.test.type.X is interface? true
Simple name: X
Canonical name: com.itzhai.test.type.X
Class name: com.itzhai.test.type.Y is interface? true
Simple name: Y
Canonical name: com.itzhai.test.type.Y
Class name: com.itzhai.test.type.Z is interface? true
Simple name: Z
Canonical name: com.itzhai.test.type.Z
Class name: com.itzhai.test.type.Letter is interface? false
Simple name: Letter
Canonical name: com.itzhai.test.type.Letter

注意,在传递给forName()的字符串必须使用全限定名(包括包名)。

通过printInfo里面使用到的方法,你可以在运行时发现一个对象完整的类继承结构。

通过使用Class的newInstance()方法是实现“虚拟构造器”的一种途径,用来创建Class的实例,得到的是Object引用,但是引用时指向Letter对象。使用newInstance()来创建的类,必须带有默认的构造器。(而通过反射API,可以用任意的构造器来动态的创建类的对象)。

2.3、类字面常量:
除了使用getName()方法,Java还提供了另一种方法来生成对Class对象的引用,即使用类字面常量:

NewLetter.class;

此方法简单安全,编译时就受到检查,更高效。不仅可用于普通类,也可以用于接口,数组以及基本数据类型。另外,对于基本数据类型的包装器类,还有一个标准字段TYPE,TYPE字段是一个引用,执行对应的基本数据类型的Class对象。为了统一,建议都使用.class这种形式。

2.4、使用.class与使用getName()方法创建对象引用的区别:
使用.class创建时,不会自动的初始化Class对象。创建步骤如下:

(1)加载 由类加载器执行:查找字节码(通常是在classpath指定的路径中查找,但并非必须的),然后从这些字节码中创建一个Class对象。

(2)链接 将验证类中的字节码,为静态域分配存储空间,如果需要,将会解析这个类创建的对其他类的所有的引用。

(3)初始化 如果该类具有超类,则对其初始化,执行静态初始化器和静态初始化块。

初始化被延迟到了对静态方法(构造器隐式的是静态的)或者非常数静态域进行首次引用时才执行的:

class Data1{
  static final int a = 1;
  static final double b = Math.random();
  static {
    System.out.println("init Data1...");
  }
}

class Data2{
  static int a = 12;
  static {
    System.out.println("init Data2...");
  }
}

class Data3{
  static int a = 23;
  static {
    System.out.println("init Data3...");
  }
}

public class ClassTest2 {
  public static void main(String[] args){
    System.out.println("Data1.class: ");
    Class data1 = Data1.class;
    System.out.println(Data1.a); // 没有初始化Data1
    System.out.println(Data1.b); // 初始化了Data1
    System.out.println(Data2.a); // 初始化了Data2
    try {
      Class data3 = Class.forName("com.itzhai.test.type.Data3"); // 初始化了Data3
    } catch (ClassNotFoundException e) {
      System.out.println("can not found com.itzhai.test.type.Data3...");
    }
    System.out.println(Data3.a);
  }
}

输出的结果为:

Data1.class: 
1
init Data1...
0.26771085109184534
init Data2...
12
init Data3...
23

初始化有效的实现了尽可能的“惰性”。

2.5、下面是判断是否执行初始化的一些情况:
(1)class语法获得对类的引用不会引发初始化;

(2)Class.forName()产生了Class引用,立即进行了初始化;

(3)如果一个static final值是“编译器常量”,那么这个值不需要对类进行初始化就可以被读取;

(4)如果只是把一个域设置为static final还不足以确保这种行为,例如上面的:

static final double b = Math.random();

(5)如果一个static域bushifinal的,那么在对它访问时,总是要先进性链接和初始化;

2.6、泛化的Class引用:
Class引用表示的是它所指向的对象的确切类型,而该对象便是Class类的一个对象。在JavaSE5中,可以通过泛型对Class引用所指向的Class对象进行限定,并且可以让编译器强制执行额外的类型检查:

Class intCls = int.class;
// 使用泛型限定Class指向的引用
Class genIntCls = int.class;
// 没有使用泛型的Clas可以重新赋值为指向任何其他的Class对象
intCls = double.class;
// 下面的编译会出错
// genIntCls = double.class;

2.6.1、使用通配符?放松泛型的限定:

Class<&#63;> intCls = int.class;
intCls = String.class;

在JavaSE5中,Class<&#63;>优于平凡的Class,更建议使用Class<&#63;>,即便它们是等价的,因为Class<&#63;>的好处是它表示你并非是碰巧或者疏忽,而是使用了一个非具体的类引用。

为了限定Class的引用为某种类型,或者该类型的子类型可以将通配符与extends一起使用,创建一个范围:

Class<&#63; extends Number> num = int.class;
// num的引用范围为Number及其子类,所以可以按照如下赋值
num = double.class;
num = Number.class;

2.6.2、泛型下的newInstance()方法:
使用了泛型后的Class,调用newInstance()返回的对象是确切类型的,但是当你使用getSuperclass()获取泛型对应的超类的时候真正的类型会有一些限制:编译器在编译期就知道了超类的类型,但是,通过这个获取到的超类引用的newInstance()方法返回的不是精确类型,而是Object:

Dog dog = dogCls.newInstance();
abstract class Animal {
}
class Dog extends Animal{
}

// 下面的写法是错误的,只能返回 Class<&#63; super Dog>类型
// Class animalCls = dogCls.getSuperclass(); 
Class<&#63; super Dog> animalCls = dogCls.getSuperclass();
// 通过获取的超类引用,只能创建返回Object类型的对象
Object obj = animalCls.newInstance();

2.6.3、新的转型语法:cast()方法
直接看下代码:

Animal animal = new Dog();
Class dogCls = Dog.class;
Dog dog = dogCls.cast(animal);
// 或者直接使用下面的转型方法
dog = (Dog)animal;

可以发现,使用cast()方法的做了额外的工作,这种转换方法可以用在一下的情况中:在编写泛型带的时候,如果存储了Class引用,并希望通过这个Class引用来执行转型,就可以使用cast()方法。

3、类型检查 instanceof
3.1、类型转换前先做检查
编译器允许你自由的做向上转型的赋值操作,而不需要任何显示的转型操作,就好像给超类的引用赋值那样。

然而如果不使用显示的类型转换,编译器就不允许你执行向下转换赋值,这个时候我们不妨先来检查一下对象是不是某个特定类型的实例,使用到了关键字 instanceof:

if(x instanceof Dog)
  ((Dog) x).bark();

3.2、RTTI的形式:
所以,到目前为止,我们知道RTTI的形式包括:

(1)传统的类型转换 (Shape)

(2)代表对象的类型的Class对象

(3)关键字instanceof

3.3、动态的instanceof方法:
Class.isInstance方法提供给了一种动态测试对象的途径。

下面演示下instanceof和Class.isInstance的用法:

Attribute:

public interface Attribute {

}

Shape:

/**
 * 创建一个抽象类
 */
public abstract class Shape{
  // this调用了当前类的toString方法获得信息
  public void draw() { System.out.println(this + ".draw()"); }
  // 声明toString()方法为abstract,从而强制继承者需要重写该方法。
  abstract public String toString();
}

Circle:

public class Circle extends Shape implements Attribute{
  public String toString(){ return "Circle"; }
}

Square:

public class Square extends Shape{
  public String toString(){ return "Square"; }
}

Triangle:

public class Triangle extends Shape{
  public String toString(){ return "Triangle"; }
}

类型检查:

// instanceOf
Circle c = new Circle();
// 判断是否超类的实例
System.out.format("Using instanceof: %s is a shape&#63; %b\n", 
    c.toString(), c instanceof Shape);
// 判断是否Circle的实例
System.out.format("Using instanceof: %s is a circle&#63; %b\n", 
    c.toString(), c instanceof Circle);
// 判断是否超类的实例
System.out.format("Using Class.isInstance: %s is a shape&#63; %b\n", 
    c.toString(), Shape.class.isInstance(c));
// 判断是否接口的实例
System.out.format("Using Class.isInstance: %s is a Attribute&#63; %b\n", 
    c.toString(), Attribute.class.isInstance(c));

可以发现,instanceof或者Class.isInstance方法判断了是否继承体系的实例,即除了判断本身,还判断是否超类或接口的实例。

下面演示下使用动态的Class.instance的用法:

首先创建一个抽象的形状生成器类:

public abstract class ShapeCreator {
  private Random rand = new Random(10);
  // 返回一个对象类型数组,由实现类提供,后面会看到两种实现形式,基于forName的和基于类字面常量的.class
  public abstract List> types();
  // 随机生成一个对象类型数组中的类型对象实例
  public Shape randomShape(){
    int n = rand.nextInt(types().size());
    try {
      return types().get(n).newInstance();
    } catch (InstantiationException e) {
      e.printStackTrace();
      return null;
    } catch (IllegalAccessException e) {
      e.printStackTrace();
      return null;
    }
  }
  // 生成一个随机数组
  public Shape[] createArray(int size){
    Shape[] result = new Shape[size];
    for(int i=0; i arrayList(int size){
    ArrayList result = new ArrayList();
    Collections.addAll(result, createArray(size));
    return result;
  }
}

接下来编写一个该抽象类的实现:

/**
 * forName的生成器实现
 * @author arthinking
 *
 */
public class ForNameCreator extends ShapeCreator{

  private static List> types = 
      new ArrayList>();
  private static String[] typeNames = {
    "com.itzhai.javanote.entity.Circle",
    "com.itzhai.javanote.entity.Square",
    "com.itzhai.javanote.entity.Triangle"
  };

  @SuppressWarnings("unused")
  private static void loader(){
    for(String name : typeNames){
      try {
        types.add((Class<&#63; extends Shape>)Class.forName(name));
      } catch (ClassNotFoundException e) {
        e.printStackTrace();
      }
    }
  }
  // 初始化加载所需的类型数组
  static {
    loader();
  }
  public List> types() {
    return types;
  }
}

最后写一个统计形状个数的类,里面用到了instanceof:

public class ShapeCount {

  static class ShapeCounter extends HashMap{
    public void count(String type){
      Integer quantity = get(type);
      if(quantity == null){
        put(type, 1);
      } else {
        put(type, quantity + 1);
      }
    }
  }

  // 演示通过instanceof关键字统计对象类型
  public static void countShapes(ShapeCreator creator){
    ShapeCounter counter = new ShapeCounter();
    for(Shape shape : creator.createArray(20)){
      if(shape instanceof Circle)
        counter.count("Circle");
      if(shape instanceof Square)
        counter.count("Square");
      if(shape instanceof Triangle){
        counter.count("Triangle");
      }
    }
    System.out.println(counter);
  }

  public static void main(String[] args){
    countShapes(new ForNameCreator());
  }
}

改写一下抽象类的实现,重新用类字面常量实现:

/**
 * 字面量的生成器实现
 */
public class LiteralCreator extends ShapeCreator{

  public static final List> allType = 
      Collections.unmodifiableList(Arrays.asList(Circle.class, Triangle.class, Square.class));

  public List> types(){
    return allType;
  }

  public static void main(String[] args){
    System.out.println(allType);
  }

}

现在使用Class.instance统计形状的个数如下:

/**
 * 通过使用Class.instanceof动态的测试对象,移除掉原来的ShapeCount中单调的instanceof语句
 *
 */
public class ShapeCount2 {

  private static final List> shapeTypes = LiteralCreator.allType;

  static class ShapeCounter extends HashMap{
    public void count(String type){
      Integer quantity = get(type);
      if(quantity == null){
        put(type, 1);
      } else {
        put(type, quantity + 1);
      }
    }
  }

  // 演示通过Class.isInstance()统计对象类型
  public static void countShapes(ShapeCreator creator){
    ShapeCounter counter = new ShapeCounter();
    for(Shape shape : creator.createArray(20)){
      for(Class<&#63; extends Shape> cls : shapeTypes){
        if(cls.isInstance(shape)){
          counter.count(cls.getSimpleName());
        }
      }
    }
    System.out.println(counter);
  }

  public static void main(String[] args){
    countShapes(new ForNameCreator());
  }
}

现在生成器有了两种实现,我们在这里可以添加一层外观,设置默认的实现方式:

/**
 * 现在生成器有了两种实现,我们在这里添加一层外观,设置默认的实现方式
 */
public class Shapes {

  public static final ShapeCreator creator =
      new LiteralCreator();
  public static Shape randomShape(){
    return creator.randomShape();
  }
  public static Shape[] createArray(int size){
    return creator.createArray(size);
  }
  public static ArrayList arrayList(int size){
    return creator.arrayList(size);
  }
}

3.4、instanceof与Class的等价性:
instanceof和isInstance()生成的结果完全一样,保持了类型的概念,判断是否一个类或者是这个类的派生类。

equals()与==也是一样的,而使用这个比较实际的Class对象,就没有考虑继承。

System.out.println(new Circle() instanceof Circle); // true
System.out.println(Shape.class.isInstance(new Circle())); // true
System.out.println((new Circle()).getClass() == Circle.class); // true
System.out.println((new Circle().getClass()).equals(Shape.class)); // false


推荐阅读
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • 本文介绍了Python高级网络编程及TCP/IP协议簇的OSI七层模型。首先简单介绍了七层模型的各层及其封装解封装过程。然后讨论了程序开发中涉及到的网络通信内容,主要包括TCP协议、UDP协议和IPV4协议。最后还介绍了socket编程、聊天socket实现、远程执行命令、上传文件、socketserver及其源码分析等相关内容。 ... [详细]
  • 这是原文链接:sendingformdata许多情况下,我们使用表单发送数据到服务器。服务器处理数据并返回响应给用户。这看起来很简单,但是 ... [详细]
  • Centos7.6安装Gitlab教程及注意事项
    本文介绍了在Centos7.6系统下安装Gitlab的详细教程,并提供了一些注意事项。教程包括查看系统版本、安装必要的软件包、配置防火墙等步骤。同时,还强调了使用阿里云服务器时的特殊配置需求,以及建议至少4GB的可用RAM来运行GitLab。 ... [详细]
  • 禁止程序接收鼠标事件的工具_VNC Viewer for Mac(远程桌面工具)免费版
    VNCViewerforMac是一款运行在Mac平台上的远程桌面工具,vncviewermac版可以帮助您使用Mac的键盘和鼠标来控制远程计算机,操作简 ... [详细]
  • 本文详细介绍了云服务器API接口的概念和作用,以及如何使用API接口管理云上资源和开发应用程序。通过创建实例API、调整实例配置API、关闭实例API和退还实例API等功能,可以实现云服务器的创建、配置修改和销毁等操作。对于想要学习云服务器API接口的人来说,本文提供了详细的入门指南和使用方法。如果想进一步了解相关知识或阅读更多相关文章,请关注编程笔记行业资讯频道。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • 本文介绍了Hyperledger Fabric外部链码构建与运行的相关知识,包括在Hyperledger Fabric 2.0版本之前链码构建和运行的困难性,外部构建模式的实现原理以及外部构建和运行API的使用方法。通过本文的介绍,读者可以了解到如何利用外部构建和运行的方式来实现链码的构建和运行,并且不再受限于特定的语言和部署环境。 ... [详细]
  • 图解redis的持久化存储机制RDB和AOF的原理和优缺点
    本文通过图解的方式介绍了redis的持久化存储机制RDB和AOF的原理和优缺点。RDB是将redis内存中的数据保存为快照文件,恢复速度较快但不支持拉链式快照。AOF是将操作日志保存到磁盘,实时存储数据但恢复速度较慢。文章详细分析了两种机制的优缺点,帮助读者更好地理解redis的持久化存储策略。 ... [详细]
  • Tomcat/Jetty为何选择扩展线程池而不是使用JDK原生线程池?
    本文探讨了Tomcat和Jetty选择扩展线程池而不是使用JDK原生线程池的原因。通过比较IO密集型任务和CPU密集型任务的特点,解释了为何Tomcat和Jetty需要扩展线程池来提高并发度和任务处理速度。同时,介绍了JDK原生线程池的工作流程。 ... [详细]
  • 如何在服务器主机上实现文件共享的方法和工具
    本文介绍了在服务器主机上实现文件共享的方法和工具,包括Linux主机和Windows主机的文件传输方式,Web运维和FTP/SFTP客户端运维两种方式,以及使用WinSCP工具将文件上传至Linux云服务器的操作方法。此外,还介绍了在迁移过程中需要安装迁移Agent并输入目的端服务器所在华为云的AK/SK,以及主机迁移服务会收集的源端服务器信息。 ... [详细]
  • CentOS 7部署KVM虚拟化环境之一架构介绍
    本文介绍了CentOS 7部署KVM虚拟化环境的架构,详细解释了虚拟化技术的概念和原理,包括全虚拟化和半虚拟化。同时介绍了虚拟机的概念和虚拟化软件的作用。 ... [详细]
  • [译]技术公司十年经验的职场生涯回顾
    本文是一位在技术公司工作十年的职场人士对自己职业生涯的总结回顾。她的职业规划与众不同,令人深思又有趣。其中涉及到的内容有机器学习、创新创业以及引用了女性主义者在TED演讲中的部分讲义。文章表达了对职业生涯的愿望和希望,认为人类有能力不断改善自己。 ... [详细]
  • 无线认证设置故障排除方法及注意事项
    本文介绍了解决无线认证设置故障的方法和注意事项,包括检查无线路由器工作状态、关闭手机休眠状态下的网络设置、重启路由器、更改认证类型、恢复出厂设置和手机网络设置等。通过这些方法,可以解决无线认证设置可能出现的问题,确保无线网络正常连接和上网。同时,还提供了一些注意事项,以便用户在进行无线认证设置时能够正确操作。 ... [详细]
  • 本文介绍了如何使用python从列表中删除所有的零,并将结果以列表形式输出,同时提供了示例格式。 ... [详细]
author-avatar
mobiledu2502876867
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有