การซ่อนชื่อ Java: วิธีที่ยาก


96

ฉันมีปัญหาเกี่ยวกับการซ่อนชื่อซึ่งแก้ยากมาก นี่คือเวอร์ชันที่เรียบง่ายที่อธิบายปัญหา:

มีคลาส: org.A

package org;
public class A{
     public class X{...}
     ...
     protected int net;
}

จากนั้นมีชั้นเรียน net.foo.X

package net.foo;
public class X{
     public static void doSomething();
}

และตอนนี้นี่คือคลาสที่มีปัญหาซึ่งสืบทอดมาจากAและต้องการโทรnet.foo.X.doSomething()

package com.bar;
class B extends A {

    public void doSomething(){
        net.foo.X.doSomething(); // doesn't work; package net is hidden by inherited field
        X.doSomething(); // doesn't work; type net.foo.X is hidden by inherited X
    }
}

อย่างที่คุณเห็นนี่เป็นไปไม่ได้ ฉันไม่สามารถใช้ชื่อธรรมดาได้Xเนื่องจากถูกซ่อนโดยประเภทที่สืบทอดมา ฉันไม่สามารถใช้ชื่อแบบเต็มได้net.foo.Xเนื่องจากnetถูกซ่อนโดยฟิลด์ที่สืบทอดมา

มีเพียงคลาสเท่านั้นที่Bอยู่ในฐานรหัสของฉัน ชั้นเรียนnet.foo.Xและorg.Aเป็นชั้นเรียนของห้องสมุดดังนั้นฉันจึงไม่สามารถแก้ไขได้!

ทางออกเดียวของฉันมีลักษณะดังนี้: ฉันสามารถเรียกชั้นเรียนอื่นที่จะเรียกX.doSomething(); แต่คลาสนี้จะมีอยู่เพราะการปะทะกันของชื่อเท่านั้นซึ่งดูยุ่งมาก! มีวิธีที่ฉันโดยตรงสามารถเรียกผู้ใดX.doSomething()จากB.doSomething()?

ในภาษาที่อนุญาตให้ระบุเนมสเปซส่วนกลางเช่นglobal::ใน C # หรือ::ใน C ++ ฉันสามารถขึ้นต้นnetด้วยคำนำหน้าส่วนกลางนี้ได้ แต่ Java ไม่อนุญาต


Quickfix บางอย่างอาจเป็นได้: public void help(net.foo.X x) { x.doSomething(); }และโทรหาhelp(null);
Absurd-Mind

@ Absurd-Mind: การเรียกเมธอดแบบคงที่ผ่านวัตถุที่ไม่มีอยู่จริงดูเหมือนจะยุ่งกว่าวิธีแก้ปัญหาของฉัน :) ฉันจะได้รับคำเตือนจากคอมไพเลอร์ด้วยซ้ำ แต่จริงนี่จะเป็นอีกวิธีแก้ปัญหาที่แฮ็กนอกเหนือจากของฉัน
gexicide

@JamesB: นี่จะเป็นการกระตุ้น X ผิด! net.foo.Xมีวิธีการไม่ใช่org.A.X!
gexicide

3
คุณมีการสืบทอดมาจากA? การสืบทอดอาจเป็นเรื่องที่น่ารังเกียจอย่างที่คุณพบ…
Donal Fellows

2
I could call another class that in turn calls X.doSomething(); but this class would only exist because of the name clash, which seems very messy+1 สำหรับทัศนคติของรหัสที่สะอาด แต่สำหรับฉันดูเหมือนว่านี่เป็นสถานการณ์ที่คุณควรทำเพื่อแลก เพียงแค่ทำสิ่งนี้และแสดงความคิดเห็นยาว ๆ เกี่ยวกับสาเหตุที่คุณต้องทำ (อาจมีลิงก์ไปยังคำถามนี้)
sampathsris

คำตอบ:


84

คุณสามารถส่ง a nullไปยังประเภทแล้วเรียกใช้เมธอดนั้น (ซึ่งจะใช้ได้ผลเนื่องจากวัตถุเป้าหมายไม่เกี่ยวข้องกับการเรียกใช้วิธีการคงที่)

((net.foo.X) null).doSomething();

นี้มีประโยชน์ของ

  • ปราศจากผลข้างเคียง (ปัญหาในการสร้างอินสแตนซ์net.foo.X)
  • ไม่ต้องเปลี่ยนชื่ออะไรเลย (คุณสามารถตั้งเมธอดในBชื่อที่คุณต้องการได้นั่นคือสาเหตุที่import staticไม่ได้ผลในกรณีของคุณ)
  • ไม่จำเป็นต้องมีการแนะนำคลาสผู้ร่วมประชุม (แม้ว่านั่นอาจเป็นความคิดที่ดี ... ) และ
  • ไม่ต้องการค่าใช้จ่ายหรือความซับซ้อนในการทำงานกับ API การสะท้อนกลับ

ข้อเสียคือรหัสนี้น่ากลัวจริงๆ! สำหรับฉันมันสร้างคำเตือนและนั่นเป็นสิ่งที่ดีโดยทั่วไป แต่เนื่องจากมันกำลังแก้ไขปัญหาที่ไม่สามารถใช้งานได้อย่างทั่วถึงจึงเพิ่มไฟล์

@SuppressWarnings("static-access")

ที่จุดปิดล้อมที่เหมาะสม (น้อยที่สุด!) จะปิดคอมไพเลอร์


1
ทำไมจะไม่ล่ะ? คุณแค่ทำสิ่งที่ถูกต้องและสร้างรหัสใหม่
Gimby

1
การนำเข้าแบบคงที่ไม่ชัดเจนไปกว่าโซลูชันนี้และไม่ได้ผลในทุกกรณี (คุณจะเมาหากมีdoSomethingวิธีใดในลำดับชั้นของคุณ) ดังนั้นใช่วิธีที่ดีที่สุด
Voo

@Voo ฉันยอมรับอย่างอิสระว่าฉันได้ลองใช้ตัวเลือกทั้งหมดที่คนอื่นระบุว่าเป็นคำตอบที่ได้ทำซ้ำครั้งแรกว่าปัญหาเดิมคืออะไรและฉันก็ตกใจว่ามันยากแค่ไหนในการแก้ไข คำถามเดิมจะปิดตัวเลือกอื่น ๆ ที่ดีกว่าทั้งหมดอย่างเรียบร้อย
Donal Fellows

1
โหวตให้กับความคิดสร้างสรรค์ฉันจะไม่ทำสิ่งนี้ในรหัสการผลิต ฉันเชื่อว่าทิศทางคือคำตอบสำหรับปัญหานี้ (ไม่ใช่สำหรับปัญหาทั้งหมดหรือไม่)
ethanfar

ขอบคุณ Donal สำหรับการแก้ปัญหานี้ ที่จริงแล้วโซลูชันนี้เน้นจุดที่สามารถใช้ "Type" และไม่สามารถใช้ตัวแปรได้ ดังนั้นใน "cast" คอมไพเลอร์จะเลือก "Type" แม้ว่าจะมีตัวแปรที่มีชื่อเดียวกันก็ตาม สำหรับกรณีที่น่าสนใจดังกล่าวมากขึ้นฉันอยากจะแนะนำ 2 "Java ผิดพลาด" หนังสือ: books.google.co.in/books/about/... books.google.co.in/books/about/...
RRM

38

อาจเป็นวิธีที่ง่ายที่สุด (ไม่จำเป็นต้องเป็นวิธีที่ง่ายที่สุด) ในการจัดการสิ่งนี้คือการใช้คลาสที่ได้รับมอบหมาย:

import net.foo.X;
class C {
    static void doSomething() {
         X.doSomething();
    }
}

แล้ว ...

class B extends A {
    void doX(){
        C.doSomething();
    }
}

สิ่งนี้ค่อนข้างละเอียด แต่ยืดหยุ่นมาก - คุณสามารถทำให้มันทำงานในแบบที่คุณต้องการได้ นอกจากนี้ยังทำงานในลักษณะเดียวกันทั้งกับstaticวิธีการและวัตถุที่สร้างอินสแตนซ์

ข้อมูลเพิ่มเติมเกี่ยวกับออบเจ็กต์มอบหมายที่นี่: http://en.wikipedia.org/wiki/Delegation_pattern


อาจเป็นวิธีที่สะอาดที่สุดที่มีให้ ฉันอาจจะใช้มันถ้าฉันมีปัญหานั้น
LordOfThePigs

37

คุณสามารถใช้การนำเข้าแบบคงที่:

import static net.foo.X.doSomething;

class B extends A {
    void doX(){
        doSomething();
    }
}

ระวังBและAไม่มีเมธอดที่ตั้งชื่อไว้doSomething


net.foo.X.doSomething ไม่สามารถเข้าถึงแพ็คเกจได้หรือไม่? หมายความว่าคุณไม่สามารถเข้าถึงได้จากแพ็คเกจ com.bar
JamesB

2
@JamesB ใช่ แต่คำถามที่สมบูรณ์ก็ไม่สมเหตุสมผลเนื่องจากวิธีนี้ไม่สามารถเข้าถึงได้ในทางใดทางหนึ่ง ฉันคิดว่านี่เป็นข้อผิดพลาดขณะทำให้ตัวอย่างง่ายขึ้น
Absurd-Mind

1
แต่คำถามเดิมดูเหมือนจะต้องการใช้doSomethingเป็นชื่อของวิธีการในB
Donal Fellows

3
@DonalFellows: คุณพูดถูก; วิธีนี้ใช้ไม่ได้หากรหัสของฉันต้องคงอยู่ตามที่ฉันโพสต์ โชคดีที่ฉันสามารถเปลี่ยนชื่อวิธีการได้ อย่างไรก็ตามลองนึกถึงกรณีที่เมธอดของฉันไปแทนที่วิธีอื่นแล้วฉันไม่สามารถเปลี่ยนชื่อได้ ในกรณีนี้คำตอบนี้จะไม่สามารถแก้ปัญหาได้อย่างแน่นอน แต่นี่จะบังเอิญยิ่งกว่าปัญหาของฉันอยู่แล้วดังนั้นอาจจะไม่เคยมีมนุษย์คนไหนที่จะประสบปัญหาเช่นนี้กับการปะทะกันของชื่อสามคน :)
gexicide

4
เพื่อให้ชัดเจนสำหรับคนอื่น ๆ : โซลูชันนี้ใช้ไม่ได้ทันทีที่คุณมีdoSomethingที่ใดก็ได้ในลำดับชั้นการสืบทอด (เนื่องจากวิธีการแก้ไขเป้าหมายการเรียกใช้เมธอด) Knuth จึงช่วยคุณได้หากคุณต้องการเรียกใช้toStringเมธอด วิธีแก้ปัญหาที่ Donal เสนอเป็นวิธีที่อยู่ในหนังสือ Java Puzzlers ของ Bloch (ฉันคิดว่ายังไม่ได้ค้นหา) ดังนั้นเราจึงสามารถพิจารณาได้ว่าเป็นคำตอบที่เชื่อถือได้จริงๆ :-)
Voo

16

วิธีที่เหมาะสมในการทำสิ่งต่าง ๆ คือการนำเข้าแบบคงที่ แต่ในกรณีที่เลวร้ายที่สุดคุณสามารถสร้างอินสแตนซ์ของคลาสโดยใช้การสะท้อนกลับได้หากคุณทราบชื่อที่มีคุณสมบัติครบถ้วน

Java: newInstance ของคลาสที่ไม่มีตัวสร้างเริ่มต้น

จากนั้นเรียกใช้เมธอดบนอินสแตนซ์

หรือเพียงแค่เรียกใช้เมธอดด้วยการสะท้อนกลับ: เรียกใช้วิธีการคงที่โดยใช้การสะท้อน

Class<?> clazz = Class.forName("net.foo.X");
Method method = clazz.getMethod("doSomething");
Object o = method.invoke(null);

แน่นอนว่านี่เป็นรีสอร์ทสุดท้าย


2
คำตอบนี้ถือเป็นการ overkill ที่ใหญ่ที่สุดจนถึงตอนนี้ยกนิ้วให้ :)
gexicide

3
คุณควรได้รับป้ายผู้สำเร็จการศึกษาสำหรับคำตอบนี้ คุณให้คำตอบสำหรับคนยากจนเอกพจน์ที่ต้องใช้ Java เวอร์ชันโบราณและแก้ปัญหานี้ได้
Gimby

ที่จริงฉันไม่ได้คิดเกี่ยวกับความจริงที่ว่าstatic importมีการเพิ่มคุณลักษณะเฉพาะในJava 1.5. ฉันไม่อิจฉาคนที่ต้องพัฒนา 1.4 หรือต่ำกว่าฉันเคยไปครั้งเดียวและมันแย่มาก!
EpicPandaForce

1
@Gimby: ยังมี((net.foo.X)null).doSomething()วิธีแก้ปัญหาที่ใช้งานได้ใน java เก่า แต่ถ้าประเภทAแม้จะมีประเภทภายในnet, แล้วนี้เป็นคำตอบเดียวที่ยังคงถูกต้อง :)
gexicide

6

ไม่ใช่คำตอบจริงๆ แต่คุณสามารถสร้างอินสแตนซ์ของ X และเรียกวิธีการคงที่ได้ นั่นจะเป็นวิธี (ฉันยอมรับว่าสกปรก) ในการเรียกวิธีการของคุณ

(new net.foo.X()).doSomething();

3
การแคสต์nullไปที่ประเภทแล้วเรียกใช้เมธอดนั้นจะดีกว่าเนื่องจากการสร้างวัตถุอาจมีผลข้างเคียงซึ่งฉันไม่ต้องการ
gexicide

@gexicide ความคิดในการคัดเลือกนักแสดงnullดูเหมือนจะเป็นวิธีที่สะอาดกว่าวิธีหนึ่ง ต้อง@SuppressWarnings("static-access")ปราศจากคำเตือน…
Donal Fellows

ขอบคุณสำหรับความแม่นยำnullแต่การคัดเลือกนักแสดงnullสามารถทำลายหัวใจของฉันได้เสมอ
Michael Laffargue

4

ไม่จำเป็นต้องทำการแคสต์หรือระงับคำเตือนแปลก ๆ หรือสร้างอินสแตนซ์ที่ซ้ำซ้อน เพียงเคล็ดลับโดยใช้ข้อเท็จจริงที่ว่าคุณสามารถเรียกใช้เมธอดแบบคงที่คลาสแม่ผ่านคลาสย่อย (คล้ายกับวิธีแก้ปัญหาแฮ็กของฉันที่นี่ )

เพียงแค่สร้างคลาสแบบนี้

public final class XX extends X {
    private XX(){
    }
}

(ตัวสร้างไพรเวตในคลาสสุดท้ายนี้ทำให้แน่ใจว่าไม่มีใครสามารถสร้างอินสแตนซ์ของคลาสนี้โดยไม่ได้ตั้งใจ)

จากนั้นคุณสามารถโทรได้ฟรี X.doSomething()ผ่านได้ฟรี:

    public class B extends A {

        public void doSomething() {
            XX.doSomething();
        }

น่าสนใจอีกวิธีหนึ่ง อย่างไรก็ตามคลาสที่มีวิธีการคงที่เป็นคลาสยูทิลิตี้ขั้นสุดท้าย
gexicide

ใช่ไม่สามารถใช้เคล็ดลับนี้ในกรณีนั้น อีกสาเหตุหนึ่งที่ทำให้วิธีการแบบคงที่เป็นภาษาที่ล้มเหลวและควรใช้วิธีการวัตถุของ Scala
billc.cn

หากคุณกำลังจะสร้างคลาสอื่นต่อไปการใช้คลาสตัวแทนตามที่อธิบายไว้ในคำตอบ blgt น่าจะดีที่สุด
LordOfThePigs

@LordOfThePigs สิ่งนี้จะหลีกเลี่ยงการเรียกเมธอดพิเศษและภาระการปรับโครงสร้างในอนาคต โดยพื้นฐานแล้วคลาสใหม่จะทำหน้าที่เป็นนามแฝง
billc.cn

2

จะเกิดอะไรขึ้นถ้าคุณลองรับ gobalnamespace เนื่องจากไฟล์ทั้งหมดอยู่ในโฟลเดอร์เดียวกัน ( http://www.beanshell.org/javadoc/bsh/class-use/NameSpace.html )

    package com.bar;
      class B extends A {

       public void doSomething(){
         com.bar.getGlobal().net.foo.X.doSomething(); // drill down from the top...

         }
     }

วิธีนี้ทำงานอย่างไร? AFAIK getGlobal()ไม่ใช่วิธี Java มาตรฐานสำหรับแพ็คเกจ ... (ฉันคิดว่าแพ็คเกจไม่สามารถมีเมธอดใด ๆ ใน Java ... )
siegi

1

นี่เป็นสาเหตุหนึ่งที่การจัดองค์ประกอบเป็นที่นิยมในการถ่ายทอดทางพันธุกรรม

package com.bar;
import java.util.concurrent.Callable;
public class C implements Callable<org.A>
{
    private class B extends org.A{
    public void doSomething(){
        C.this.doSomething();
    }
    }

    private void doSomething(){
    net.foo.X.doSomething();
    }

    public org.A call(){
    return new B();
    }
}

0

ฉันจะใช้รูปแบบกลยุทธ์

public interface SomethingStrategy {

   void doSomething();
}

public class XSomethingStrategy implements SomethingStrategy {

    import net.foo.X;

    @Override
    void doSomething(){
        X.doSomething();
    }
}

class B extends A {

    private final SomethingStrategy strategy;

    public B(final SomethingStrategy strategy){
       this.strategy = strategy;
    }

    public void doSomething(){

        strategy.doSomething();
    }
}

ตอนนี้คุณได้แยกการพึ่งพาแล้วดังนั้นการทดสอบหน่วยของคุณจะเขียนได้ง่ายขึ้น


คุณลืมที่จะเพิ่มimplements SomethingStrategyในXSomethingStrategyการประกาศคลาส
Ricardo Souza

@rcdmk ขอบคุณครับ. ควรได้รับการแก้ไขในขณะนี้
Erik Madsen
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.